Files
cloud-ip-validator/docs/changes/2026-10-03_16-39_analytics-section-plan.md
T
ayurishchevandClaude Sonnet 5.5 db73409e8f Add plans for the analytics section and for verdicts without late results
- docs/changes: plan of the "Аналитика" section (runs as selectable
  timestamps, API, page layout, acceptance numbers from the 6440-address run)
  and plan of the verdict-integrity change (causes found in the code and in
  the data, decisions, tests, rollout).
- docs/mockups/analytics-mockup.html: self-contained HTML mockup of the
  analytics page for review in a browser.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 17:59:40 +03:00

142 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: раздел «Аналитика»
Статус: план, код не менялся. Основа — прогон 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`). Это единственное изменение схемы, без него переключатель невозможен.
## 3. Модель данных (миграция `0010`)
Новые таблицы:
- `check_runs(id, kind, started_at, finalized_at, state)`. `kind`: `auto` (автоцикл), `manual` (добавление, скан, перепроверка). `state`: `open` или `finalized`.
- `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)` запуска, чтобы поздний результат сбросил кэш.
## 5. Макет страницы `/analytics?run=<id>`
```
┌─ Аналитика ─────────────────────────────────────────────────────────────┐
│ Запуск: ◀ [ 02.10 13:47 → 22:29 · авто · 6440 адр · 30% pass ▾ ] ▶ │
│ (идущий запуск виден в списке, но недоступен: «идёт, 120 из 800») │
├──────────────────────────────────────────────────────────────────────────┤
│ [Адресов 6440] [pass 1962 · 30%] [partial 4478 · 70%] [fail 0] │
│ [Egress OK 31%] [Ingress OK 97%] [Длительность 8 ч 42 м] [Поздних 246] │
├──────────────────────────────────┬───────────────────────────────────────┤
│ Почему partial │ Данные: качество │
│ ▇▇▇▇▇▇▇▇▇▇▇▇ только egress 4146 │ Поздние результаты 246 (50 адр.)│
│ ▇ ingress+egress 157 │ Неполный набор 167 адресов │
│ ▇ egress+неполный набор 125 │ Вердикт ≠ проверки 50 адресов │
│ ▏ только ingress 8 │ │
├──────────────────────────────────┴───────────────────────────────────────┤
│ Подсети [сорт: хуже всего ▾] [показать все 43] │
│ подсеть адр. pass% Egress OK% Ingress OK% │
│ 161.104.108.0/23 45 ░░░░░ 0% ░░░░░ 0% ▇▇▇▇▇ 100% │
│ 83.166.248.0/21 659 ▇░░░░ 11% ▇░░░░ 12% ▇▇▇▇░ 91% →реестр│
├──────────────────────────────────────────────────────────────────────────┤
│ Egress по целям тип: [https] [icmp] [все] │
│ цель провал | подсеть × цель (тепловая карта) │
│ packages.ubuntu.com 60% | 83.166.248.0/21 ■■□■■ │
│ dl-cdn.alpinelinux.org 34% | 212.233.72.0/21 ■■□■■ │
├──────────────────────────────────────────────────────────────────────────┤
│ Ingress по площадкам площадка × тип (tcp/ssh/icmp) | классы ошибок │
├──────────────────────────────────────────────────────────────────────────┤
│ Валидаторы: полоса «доля провалов egress» по 20 валидаторам (≈33–38%) │
└──────────────────────────────────────────────────────────────────────────┘
```
- Два индикатора прикладного уровня (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`).
## 6. Файлы
- БД: `internal/db/migrations/0010_check_runs.sql`, `models.go`, `queries_runs.go`, правки `queries_ipqueue.go` (`SubmitIPs`, `FinishIP`, `CancelIP`, `ClearAllIPs`) и `queries_checks.go` (`UpsertCheck` ставит `run_id`).
- Оркестратор: `internal/orchestrator/orchestrator.go` — запись `run_results` при агрегации, завершение запуска; автоцикл открывает запуск.
- Аналитика: новый пакет `internal/analytics` (расчёт блоков, классификатор ошибок, подсети).
- API: `internal/httpapi/handlers_analytics.go`, `routes.go`, `docs/API.md`.
- Дашборд: `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/` → обновление графа.