diff --git a/docs/changes/2026-10-03_16-39_analytics-section-plan.md b/docs/changes/2026-10-03_16-39_analytics-section-plan.md new file mode 100644 index 0000000..42c0a65 --- /dev/null +++ b/docs/changes/2026-10-03_16-39_analytics-section-plan.md @@ -0,0 +1,141 @@ +# План: раздел «Аналитика» + +Статус: план, код не менялся. Основа — прогон 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=` + +``` +┌─ Аналитика ─────────────────────────────────────────────────────────────┐ +│ Запуск: ◀ [ 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/` → обновление графа. diff --git a/docs/changes/2026-10-03_17-21_verdict-no-late-results-plan.md b/docs/changes/2026-10-03_17-21_verdict-no-late-results-plan.md new file mode 100644 index 0000000..d35e86a --- /dev/null +++ b/docs/changes/2026-10-03_17-21_verdict-no-late-results-plan.md @@ -0,0 +1,83 @@ +# План: вердикт без опоздавших результатов + +Статус: план, код не менялся. Основа: код `internal/orchestrator`, `internal/probercore`, `internal/httpapi` и БД стенда (запуск 2026-10-02, 6440 адресов). + +## 1. Требование + +Вердикт адреса и его проверки должны совпадать. После вердикта результат не может появиться или измениться. Вердикт можно пересчитать из сохранённых проверок и получить то же значение. + +## 2. Причины (проверены по коду и данным) + +1. **Prober проверяет один адрес много раз.** `GET /probers/{site}/assignments` отдаёт все адреса в состоянии `checking`. Prober (`probercore.pollOnce`) обходит их при каждом опросе (каждые 5 с) и ничего не помнит о том, что адрес уже проверен. Пока адрес ждёт остальные площадки и egress, он проверяется снова и снова. +2. **Новый результат затирает старый.** Результаты пишутся через `UpsertCheck` с `ON CONFLICT DO UPDATE`. Строка живёт с первой записи, а значения берутся из последнего круга. В БД 61 406 из 76 644 ingress-строк (80%) перезаписаны позже первой записи более чем на 3 с. У egress перезаписей нет (агент проверяет один раз). +3. **Записи принимаются после вердикта.** `Orchestrator.RecordCheck` вызывает `UpsertCheck` без проверки состояния адреса. Итог на запуске 02.10: + - ingress: 1430 строк записаны после вердикта (844 проваленных и 586 успешных); из 844 проваленных 833 записаны уже после отвязки Floating IP, то есть проверяли адрес, который к валидатору уже не привязан; + - egress: 351 строка после вердикта (32 проваленных). +4. **Окно считается не с начала проверки.** `isReadyToAggregate` сравнивает `assigned_at` с порогом `checking_window_seconds` (120 с). `assigned_at` ставится при выдаче адреса валидатору, то есть окно включает привязку Floating IP, паузу `fip_settle_seconds` (30 с) и self-check (до 60 с). На сами проверки остаётся около 30 с. По окну агрегировано 509 адресов (8%), у них часть результатов не успела прийти. +5. **Время сравнивается чужими часами.** `checks.checked_at` ставит prober или агент на своей машине, а `aggregated_at` — control-api. При расхождении часов «после вердикта» определяется неверно. + +Следствие: часть проваленных ingress-проверок в БД — не провалы адреса, а результат проверки уже отвязанного Floating IP. Отчёты по провалам ingress (в том числе таймаутам) за запуск 02.10 нужно читать с этой поправкой. + +## 3. Решение + +### A. Одна проверка адреса на площадку и попытку +`handleProberAssignments` не отдаёт адрес, для которого эта площадка уже отметила `complete` в `ip_site_checks` в текущей попытке. Новая попытка (повтор после сбоя) отдаёт адрес снова. Prober менять не нужно, пересобирается только `control-api`. Побочный эффект: нагрузка на prober и на сеть падает в разы. + +### B. После вердикта запись закрыта +`UpsertCheck` пишет строку только если адрес в состоянии `checking` и номер попытки совпадает. Проверка выполняется в одном SQL-запросе (условие в `INSERT … SELECT … WHERE EXISTS`), без гонок. +- Отброшенный результат не пишется и не меняет существующую строку. +- Ответ агенту и prober остаётся `200 {"ok": true, "ignored": true}`, чтобы они не повторяли запрос. +- На каждую отброшенную пачку пишется одно событие `result_dropped` (источник, число проверок). Счётчик виден в аналитике; в норме он равен нулю или близок к нему. + +### C. Окно проверки считается с начала проверки +Новая колонка `ip_queue.checking_started_at`, ставится в `SetChecking`. `isReadyToAggregate` сравнивает порог с ней, а не с `assigned_at`. Значение `checking_window_seconds` не меняется (120 с), но теперь это 120 с именно на проверки. Для строк без значения (старые данные) используется `assigned_at`. + +### D. Время записи ставит сервер +Новая колонка `checks.recorded_at` — время последней записи по часам control-api; при перезаписи обновляется. Все сравнения «до/после вердикта» идут по ней, а не по `checked_at`. Для старых строк значение равно `created_at`. + +### E. Вердикт воспроизводим +При агрегации в событие `aggregated` дописываются `egress_total`, `ingress_total`. Тест-инвариант: вердикт, пересчитанный по сохранённым проверкам адреса с учётом недостающих, равен `overall_result`. + +### F. Недостающие результаты +Правило не меняется: недостающая проверка считается провалом, вердикт не выше `partial`. Благодаря C число таких адресов должно упасть. Остаток показывается отдельно («нет результата»), а не смешивается с провалами. + +### G. Старые данные +В миграции `0010` колонка `checks.after_verdict` (0/1) для существующих строк: `1`, если `checked_at` позже `aggregated_at` строки очереди. Пересчитать затёртые результаты нельзя, поэтому в аналитике они помечаются как недостоверные и показываются отдельно (на запуске 02.10: 1430 ingress и 351 egress). После внедрения для новых данных значение всегда `0`. + +Отвергнутый вариант: пересчитывать вердикт при опоздавших результатах. К этому моменту Floating IP уже отвязан, и такие результаты ничего не доказывают. + +## 4. Файлы + +- `internal/db/migrations/0010_verdict_integrity.sql` — колонки `ip_queue.checking_started_at`, `checks.recorded_at`, `checks.after_verdict`. +- `internal/db/queries_checks.go` — условная запись, признак «отброшено». +- `internal/db/queries_ipqueue.go` — `SetChecking`, выборка для prober без адресов с готовой площадкой. +- `internal/orchestrator/orchestrator.go` — `RecordCheck` возвращает признак, `isReadyToAggregate` по `checking_started_at`, поля в событии `aggregated`. +- `internal/httpapi/handlers_agent.go`, `handlers_prober.go` — ответ `ignored`, событие `result_dropped`. +- Документы: `docs/API.md` (ответы на запись результатов), `docs/USAGE.md`, `README.md`. +- Нумерация миграций: этот план идёт раньше плана аналитики (`…_analytics-section-plan.md`); аналитика получает `0011`. + +## 5. Тесты + +- Запись после вердикта игнорируется: строка не создана и не изменена; запись в состоянии `checking` проходит; запись старой попытки игнорируется. +- Повторная запись той же проверки внутри окна идемпотентна. +- `assignments`: после `complete` площадки адрес не возвращается ей, но возвращается остальным; после повтора попытки возвращается снова. +- Окно считается с `checking_started_at`: адрес, назначенный давно, но начавший проверку только что, не агрегируется по окну. +- Инвариант вердикта: для набора сценариев (все успешны, часть провалена, часть недостающих, ничего) вердикт равен пересчёту. +- Миграция на копии боевой БД: `after_verdict` = 1 для 1430 ingress и 351 egress строк запуска 02.10, остальное без изменений; повторный запуск безопасен. + +## 6. Проверка на стенде + +1. Копия `rxprod-compose/capi-db/control-api.db`, пересборка и перезапуск `control-api` (по процедуре из `docs/SETUP.md`). Prober и агенты не пересобираются. +2. Контрольный прогон на небольшой партии (например, 300 адресов). +3. Ожидаемый результат: ни одной строки `checks` с `recorded_at` позже `aggregated_at` (`after_verdict = 0`); нет перезаписанных ingress-строк (`recorded_at` равен первой записи); событий `result_dropped` нет или единицы; адресов с агрегацией по окну заметно меньше 8%. +4. Сравнение с запуском 02.10: доля проваленных ingress-проверок (таймауты ssh, tcp-22, icmp) должна уменьшиться, если часть прежних провалов была отвязанным Floating IP. Если не уменьшится, версия про отвязку неверна, и нужно искать другую причину. + +## 7. Риски + +- Отброшенные результаты теряются. Это сознательно: после вердикта Floating IP отвязан. Счётчик `result_dropped` показывает, насколько часто это случается. +- Уменьшение числа проверок prober меняет нагрузку на сеть площадок. Это ожидаемый эффект, а не риск, но стоит предупредить владельцев площадок. +- Новое окно (с начала проверки) может чуть увеличить длительность прогона на адресах, у которых действительно не пришли результаты. Значение окна настраивается. + +## 8. Порядок по правилам проекта + +План (этот файл) → реализация → `…-summary.md` → обновление `README.md` и `docs/` → пересборка стенда → контрольный прогон → обновление графа. Раздел «Аналитика» (план `2026-10-03_16-39`) строится после этого изменения: «Поздние результаты» там становятся контрольным показателем, который должен быть равен нулю. diff --git a/docs/mockups/analytics-mockup.html b/docs/mockups/analytics-mockup.html new file mode 100644 index 0000000..9449a55 --- /dev/null +++ b/docs/mockups/analytics-mockup.html @@ -0,0 +1,498 @@ + + +Аналитика запусков + + + + + +
+ + +
+
Макет. Запуск от 02.10 построен на реальных числах из отчётов analysis/ (6440 адресов). Два других запуска и идущий — демонстрационные, нужны только для проверки переключателя.
+ +
+

Аналитика

+

Один выбранный запуск. Данные других запусков на странице не участвуют.

+
+ +
+
+ + + + +
+

+
+ +
+ +
+
+

Почему partial

+
+

Адрес считается один раз, по главной причине. Всё, где есть egress, учитывается как egress.

+
+
+

Качество данных

+
+

Вердикт ставится при агрегации. Результаты, пришедшие позже, остаются в этом же запуске, но в вердикт не входят.

+
+
+ +
+
+

Подсети

+
+
+ + +
+ +
+
+
+

Строка ведёт в «Реестр» с фильтром по запуску и подсети.

+
+ +
+
+

Egress по целям

+
+ + +
+
+
+
+

Подсеть × цель

+
+
+
+

+
+ +
+
+

Ingress по площадкам

+
+
+
+

Классы ошибок ingress

+
+
+
+ +
+

Валидаторы: доля провалов egress https

+ +
validator 1validator 20
+

Ровная полоса значит: проблема зависит от подсети адреса, а не от валидатора.

+
+
+
+ +
+

+

+ +
+
+ +
+ + + +
+
+
+
+ + + +