Registry and Analytics: run, subnet, direction and protocol filters, successes-by-target chart

Registry (/registry):
- filters by run (slice by the address's cycle in that run), subnet
  (drop-down of configured subnets), direction (egress/ingress) and
  protocol (icmp, tcp, ssh, https, tls); status in scope is computed over
  the narrowed checks
- chart "successful checks per target (egress) / site (ingress)" when both
  direction and protocol are chosen; a row opens the list of addresses
  (dialog, CSV)
- API: direction/protocol parameters and run in GET /admin/registry,
  GET /admin/registry/breakdown and /breakdown/list
- subnet filter passes ids as one JSON parameter (SQLite variable limit)

Analytics (/analytics):
- subnet filter recomputes the whole page over the addresses of the run
  inside the subnet; only their checks are read; cache per run and subnet
- direction and protocol focus the page; with both set the registry chart
  is shown
- subnet parameter in GET /admin/analytics/runs/{id} and lists (JSON, CSV)

Docs: plans and summaries in docs/changes, README, API, USAGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-06 14:23:48 +03:00
1 parent 068c10ea1c
commit ded196ec8d
40 files changed
+2545 -188

No files matched your search

@@ -0,0 +1,74 @@
# План: фильтры «Запуск», «Подсеть», «Направление», «Протокол» в разделе «Реестр»
Редакция 3 (2026-10-06): добавлен выбор запуска (задачи сканирования) из накопленной истории; в список протоколов добавлен `tls`.
## Задача
В `/registry` добавить четыре фильтра; существующие (поиск по IP, статус, «на странице») сохраняются:
1. **Запуск** — выбор «цикла сканирования» (задачи) из истории запусков.
2. **Подсеть** — показать адреса реестра, входящие в подсеть.
3. **Направление** — Egress или Ingress.
4. **Протокол** — icmp, tcp, ssh, https, tls.
## Что уже есть (граф + README + код)
- **Запуск = `check_runs`** (миграция 0011): одна задача сканирования, ручная или автоцикла, со списком адресов (`run_results`: адрес, `cycle_id`, вердикт) и привязкой проверок (`checks.run_id`, индекс `idx_checks_run(run_id, registry_id, cycle_id)`). История копируется, пока её не очистили по `docs/ADMIN_CLEANUP.md`. Список запусков уже отдаёт `GET /admin/analytics/runs` (клиент `ListAnalyticsRuns`, подпись `runLabel`) — используем его же. Номер цикла `cycle_id` считается по адресу и запуск не определяет, поэтому выбираем именно запуск.
- **Фильтр `run` уже есть**, но неполный: `RegistryFilter.RunID` лишь ограничивает список адресами запуска (`queries_registry.go:187`), а столбец «Последний результат» по-прежнему показывает **последний цикл адреса**, а не результат в выбранном запуске. Сейчас он включается только переходом из аналитики, элемента выбора нет.
- **Подсеть:** фильтр есть в API и дашборде (`subnet`, `subnetIDs`), UI-элемента нет. Список подсетей с метками есть (`GetSubnets`).
- **Направление и протокол** в БД отдельных колонок не имеют: выводятся из `checks.source` и `checks.check_type`, правила — `CheckLevel`, `CheckFamily` (`models.go:84-106`). Миграция не нужна.
- Уровни Egress/Ingress и разбивка по типам в таблице уже показываются (`fillRegistryLevels`).
## Решения (приняты по умолчанию — подтвердить)
1. **Запуск задаёт срез данных.** Фильтры направления/протокола/статуса и столбец «Результат» считаются по циклу адреса **в выбранном запуске** (`run_results.cycle_id`). Без запуска — по последнему циклу адреса, как сейчас. Выбранный запуск также ограничивает список его адресами.
2. **Статус в запуске** — это вердикт из `run_results.verdict` (не сегодняшний `ip_queue.overall_result`). Так таблица совпадает со страницей «Аналитика» за этот запуск. Статус `cancelled` доступен только здесь и без области направления/протокола.
3. **Направление и протокол сужают область проверок**, работают вместе со статусом. Пример: запуск 3 + Ingress + https + fail = адреса, у которых в запуске 3 все проверки https на входе неуспешны. Без статуса — адреса, у которых такие проверки есть. Статус в области: все успешны — `pass`, ни одной — `fail`, иначе `partial` (по проверкам области, а не вердикту).
4. **Протоколы** — по семейству: `tcp` = `tcp-22`, `tcp-443` и т. д. В UI пять значений: `icmp`, `tcp`, `ssh`, `https`, `tls`. `tls` — входящая проверка (`tls-443`, TLS-хендшейк пробера на 443); исходящих `tls` нет, поэтому `Egress` + `tls` даёт пустой результат — это ожидаемо (подсказка в UI: «tls проверяется только на входе»). Позволяет найти адреса, где TCP на 443 открыт, а TLS не поднимается.
5. **Выбор запуска** — выпадающий список «Последний цикл (по умолчанию)» + запуски, новые первыми, подпись `runLabel`. Открытые (идущие) запуски доступны: данные частичные, это видно в подписи («идёт»), в отличие от аналитики, где они недоступны.
6. **Подсеть** — выпадающий список настроенных подсетей (с меткой) плюс ручной ввод CIDR (`<input list>`). Невалидный CIDR игнорируется.
7. **Таблица в области** показывает только затронутые уровни и типы (при Ingress скрыт Egress, при `https` — только чипы https). Заголовок столбца: «Последний результат» или «Результат в запуске N».
## Изменения
### 1. БД (`internal/db`)
- `RegistryFilter`: добавить `Level` (`egress|ingress`), `Family` (`icmp|tcp|ssh|https|tls`); `RunID` остаётся.
- `ListRegistryPage` (общая логика выбора цикла адреса `scopeCycle`):
- без запуска: цикл = `MAX(cycle_id)` адреса (как сейчас);
- с запуском: список — `run_results WHERE run_id=?`, цикл = `run_results.cycle_id`, вердикт = `run_results.verdict`; проверки берутся с `checks.run_id=?` и этим циклом (идёт по `idx_checks_run`).
- условие статуса: без области — по `lastResultCond` (без запуска) или `run_results.verdict` (с запуском); с областью — `CASE SUM(success)…` по проверкам области.
- условие области (`EXISTS`/`CASE`): `source='egress'` или `LIKE 'inbound-site-%'`, `check_type = ? OR LIKE ?||'-%'`.
- `fillRegistrySummary` / `fillRegistryLevels`: принимать срез (запуск, уровень, семейство) и заполнять `LastResult`, `LastCycleID`, `Egress`, `Ingress` из него. Без среза — как сейчас, поведение прежнее.
- `subnetIDs`: передавать идентификаторы одним JSON-параметром (`r.id IN (SELECT value FROM json_each(?))`) вместо `?,?,?…`; сейчас при >32 766 адресов в подсети запрос упадёт по лимиту SQLite. Проверить `json_each` в `modernc.org/sqlite` v1.57.
- Неизвестные значения → `ErrValidation`; неизвестный `run` → пустой список.
### 2. HTTP API (`internal/httpapi`)
- `GET /admin/registry`: новые параметры `direction` (`egress|ingress`) и `protocol` (`icmp|tcp|ssh|https|tls`); неверные → 400 с перечнем. Параметры `run` и `subnet` уже есть. Поля ответа прежние, но при `run` они отражают результат в запуске; в ответ страницы добавить `run` (id среза или 0).
- `docs/API.md` — раздел реестра: параметры, смысл `run`, пример.
### 3. Дашборд (`internal/dashboard`)
- `client.go`: `registryQuery` + `Direction`, `Protocol`.
- `handlers_registry.go`: читать `run`, `subnet`, `direction`, `protocol` из URL, проверять по белому списку, класть в `params` пагинатора (фильтры сохраняются при листании и в адресной строке). Данные для выбора: `ListAnalyticsRuns` и `GetSubnets`. Ошибка получения списков не ломает страницу: фильтр остаётся текстовым/без подписей, показывается баннер.
- `templates/registry.html`: в форму `#registry-filter` — четыре поля «Запуск», «Подсеть», «Направление», «Протокол» с теми же `hx-get`/`hx-include`/`hx-replace-url`, что у существующих; скрытые поля `run`/`subnet` и плашка «Из аналитики» заменяются видимыми полями (ссылка «к аналитике» остаётся при выбранном запуске, кнопка «сбросить фильтры»). Подпись «найдено N адресов». `registry_level` скрывает уровни и типы вне области. Сообщение «Ничего не найдено» учитывает все фильтры.
- Ссылки из аналитики (`/registry?run=&subnet=`) продолжают работать.
### 4. Тесты (минимум)
- `internal/db`: один табличный тест среза — запуск (два запуска одного адреса дают разный результат), направление, протокол (`tcp-22` и `tcp-443` → `tcp`, `tls-443` → `tls`, `tls` на Egress — пусто), статус в области, подсеть + запуск + направление вместе, адрес без проверок в области исключается.
- `internal/httpapi`: 400 на неверные `direction`/`protocol`; новые параметры вместе со старыми.
- `internal/dashboard` (`TestRegistryPageAndDetail`): страница с четырьмя фильтрами, сохранение значений в пагинаторе.
- `TestScaleSmoke6440`: прогон со всеми фильтрами, лимит 10 с.
## Порядок работы
1. Утвердить план и пункты «Решения».
2. Субагент (Sonnet 5.5, Medium effort) пишет код: п.1 → п.2 → п.3 → п.4.
3. `go build ./... && go vet ./... && go test ./...`; `EXPLAIN QUERY PLAN` запросов среза — по `idx_checks_run` и `idx_checks_registry_cycle`; страница на 6440 адресов — не медленнее текущей более чем в разы.
4. Проверка на стенде (`docs/LOCAL_E2E.md`): сверить цифры запуска в `/registry` со страницей «Аналитика» этого же запуска.
5. Summary в `docs/changes/…-summary.md`; обновить `README.md` (строки «Реестр», журнал изменений), `docs/API.md`, `docs/USAGE.md`; обновить граф `/graphify . --update`.
## Риски
- Для данных, накопленных до появления запусков, запуски выделены по паузам (миграция 0011) — граница приблизительная.
- Если история циклов обрезана (`history_retention_cycles`), проверки старого запуска могут быть удалены: адрес остаётся в `run_results`, но уровни пусты. Показывать «—» и подсказку.
- Статус в области и вердикт могут расходиться (вердикт учитывает недостающие результаты). Подсказка возле фильтра.
- Подзапрос цикла на адрес при 6440+ строк: проверить планом; при деградации заменить одним агрегирующим запросом по `checks` с `GROUP BY registry_id`.
@@ -0,0 +1,37 @@
# Итог: фильтры «Запуск», «Подсеть», «Направление», «Протокол» в «Реестре»
План: [2026-10-06_12-12_registry-subnet-direction-protocol-filters-plan.md](2026-10-06_12-12_registry-subnet-direction-protocol-filters-plan.md).
Статус: код написан и проверен (gofmt, build, vet, test, замеры на копии данных стенда); стенд **не пересобирался**, страница в браузере не открывалась.
## Что изменено
- **БД** (`internal/db/queries_registry.go`): `RegistryFilter.Level` и `Family`; общий «срез» данных адреса (запуск, направление, протокол). С запуском список берётся из `run_results`, цикл и вердикт — оттуда же, проверки — по `checks.run_id`. Без запуска — последний цикл, как раньше. Направление и протокол сужают проверки цикла; статус в такой области считается по этим проверкам. `subnetIDs` передаёт id одним JSON-параметром (`json_each`) — лимит параметров SQLite больше не угрожает крупной подсети. Неверные значения — `ErrValidation`.
- **API** (`internal/httpapi`): параметры `direction` (`egress|ingress`) и `protocol` (`icmp|tcp|ssh|https|tls`) в `GET /admin/registry`, `400` на неверные; в ответ страницы добавлено поле `run`. `docs/API.md` обновлён.
- **Дашборд** (`internal/dashboard`): в форме `/registry` четыре новых поля — «Запуск» (список запусков из аналитики), «Подсеть» (ввод + список настроенных подсетей), «Направление», «Протокол»; значения в адресной строке и в ссылках пагинатора. Столбец «Результат в запуске N», строка «Найдено адресов», скрытие уровней вне области, ссылка «к аналитике запуска N», подсказка про срез и `tls`. Старая плашка «Из аналитики» заменена видимыми полями; ссылки из аналитики работают.
- **Документы**: `API.md`, `USAGE.md`, `README.md`.
## Исправлено при ревью
1. Запрос списков запусков и подсетей делался при каждом обновлении таблицы через htmx — теперь только при полной загрузке страницы (как на `/ips`).
2. Ссылки «сбросить фильтры» и «к аналитике» не обновлялись при htmx-замене таблицы: первая стала постоянной, вторая перенесена в обновляемую область (видна и при пустой выдаче).
3. Тест `TestRegistryDrillDownFromAnalytics` приведён к новой форме; длинный комментарий в `fillRegistryLevels` перенесён.
## Проверки
- `gofmt -l` пусто; `go build ./...`, `go vet ./...`, `go test -count=1 ./...` — все пакеты `ok`.
- Новые тесты: табличный `TestRegistryPageSlice` (запуск, направление, протокол, статус в области, подсеть, валидация), параметры и `400` в API, четыре фильтра в дашборде, `TestScaleSmoke6440` со всеми фильтрами.
- `EXPLAIN QUERY PLAN`: запросы среза идут по `idx_checks_run` и `idx_checks_registry_cycle`, полного перебора `checks` нет.
- Замер на копии БД стенда (6498 адресов, 1,07 млн проверок, снимок `VACUUM INTO`): страница 50 строк — 46–160 мс без направления/протокола, 350–730 мс с ними; подсеть, запуск и все фильтры вместе — 63 мс.
- Сверка с SQL вручную: «запуск 7 + Egress + https + fail» и «tcp + fail» — по 1 адресу, совпало с прямым запросом.
- В данных стенда проверок `tls` нет (входящие порты не включают 443), поэтому «tls» там даёт пустой список — это ожидаемо.
## Особенности и замечания
- Тест `TestUpsertCheckIfOpenSetsRecordedAt` (`internal/db`, не затронут изменением) один раз упал при параллельной нагрузке: он ждёт 5 мс по часам. Отдельно и в последующих прогонах проходит. Стоит увеличить паузу отдельным изменением.
- Пилюля вердикта в строке остаётся общим вердиктом даже при направлении/протоколе; статус-фильтр в области может с ним расходиться (подсказка в форме).
- Статус `cancelled` вместе с направлением или протоколом даёт пустой список.
- Неизвестный `run` в URL даёт пустую таблицу и выбранный пункт «Запуск N».
## Выкладка
Не выполнена. Нужны пересборка и перезапуск `control-api` и `admin-dashboard`; миграций нет. Выкладку делать при пустой очереди (`docs`/процедура пересборки стенда).
@@ -0,0 +1,81 @@
# План: выпадающий список подсетей и чарт «успешные проверки по целям / площадкам» в «Реестре»
Редакция 2 (решения по вопросам подтверждены пользователем).
Продолжение [2026-10-06_12-12_registry-subnet-direction-protocol-filters](2026-10-06_12-12_registry-subnet-direction-protocol-filters-plan.md). Фильтры работают; улучшаем удобство и восприятие.
## Задача
1. **Подсеть** выбирается из выпадающего списка (сейчас — поле ввода с подсказками).
2. Когда выбраны **направление и протокол** (например, Egress + https), над таблицей показывается чарт «количество успешных проверок» по каждой цели 1…N:
- **Egress** — в разрезе целей;
- **Ingress** — в разрезе площадок (та же логика).
3. Строка чарта кликабельна и открывает список адресов с детализацией.
4. Подход переиспользуем из «Аналитика → Egress по целям».
## Что уже есть (граф + код)
- Аналитика: блок «Egress по целям» — строки `an-bar-row` (название, полоса `an-track`/`an-fill`, число и процент), вкладки по типу проверки. Диалог со списком адресов (`analytics-dialog.js`, шаблон `analytics_dialog`, «Копировать», «Скачать CSV», подсказки при наведении) подключается страницей и уже обслуживает списки по запросу `load(url)` → `fill(...)`. Стили — `static/analytics.css`.
- Данные в БД: в `checks` для egress `target` — адрес цели (`https://github.com`), `source='egress'`. Для ingress `source = inbound-site-N` (площадка), а `target` — сам проверяемый адрес, поэтому ingress группируется по `source`, а не по `target`. Имена площадок берутся по индексу (как `SiteNames` в аналитике), без имени — `site-N`.
- Срез данных (запуск / последний цикл, направление, протокол, статус, подсеть) уже реализован в `internal/db/queries_registry.go` (`registrySlice`, `scopeFrom`). Чарт строится по тому же срезу и тому же набору адресов, что и таблица.
- Список настроенных подсетей уже грузится в форму (`GetSubnets`), 45 штук на стенде.
- Таблица `/registry` обновляется через htmx (`#registry-table-wrap`).
## Решения (подтверждены пользователем)
1. **Когда показывать чарт:** выбраны **и** направление, **и** протокол. С одним направлением вместо чарта — короткая подсказка «Выберите протокол, чтобы увидеть распределение по целям/площадкам».
2. **Набор адресов — все под текущим фильтром** (запуск, подсеть, статус, поиск), а не только текущая страница. Чарт совпадает с «Найдено адресов: N». Чарт строится в разрезе проверок выбранного типа в рамках проверочного цикла среза: цикл адреса в выбранном запуске или его последний цикл.
3. **Что считается:** число записанных проверок выбранного типа в проверочном цикле среза: `успешных из всего`. Полоса — доля успешных; подпись: `1 234 из 1 250 · 98,7%`. У tcp и tls на ingress у адреса может быть несколько проверок на площадку (порты 22 и 443), поэтому считаются именно проверки, а не адреса (пояснение в подписи под чартом).
4. **Порядок строк:** **от большего к меньшему** по числу успешных проверок, при равенстве — по названию. Полоса масштабируется по наибольшему значению; доля успешных остаётся в подписи.
5. **Подпись цели:** адрес цели без схемы и без завершающего `/` (`repo.almalinux.org/almalinux`), ключ — исходное значение `target`. Разные пути на одном хосте остаются разными строками.
6. **Клик по строке:** диалог со списком **всех** адресов набора, у которых есть проверки этой цели/площадки, провалы сверху (порядок списка не связан с порядком чарта). Столбцы: Адрес, Результат (`успешно`/`провал`), Тип проверки (`https`, `tcp-22`…), Цель или Площадка, Валидатор (для egress), Задержка (мс), Детали (ошибка), Проверено. Сверху — счётчики «успешно N · провал M». Доступны «Копировать» и «Скачать CSV».
7. **Подсеть в списке** (ручной ввод убирается, решение подтверждено): варианты — «Все подсети» + настроенные подсети (`CIDR — метка`). Ручной ввод убирается. Подсеть из URL (переход из аналитики), которой нет в списке, добавляется отдельным выбранным пунктом. Если подсети не настроены — список недоступен, рядом ссылка на `/settings` с их настройкой.
8. **Egress + ssh/tcp/tls** (таких проверок нет): чарт показывает «Для этого сочетания проверок нет», как и пустая таблица.
## Изменения
### 1. БД (`internal/db`)
- Выделить сборку условий `FROM`/`WHERE` из `ListRegistryPage` в функцию, которую используют и список, и новая выборка (поведение списка не меняется; существующие тесты — страховка).
- Новый файл `queries_registry_breakdown.go`:
- `RegistryBreakdown(ctx, filter) (*Breakdown, error)`: требует `Level` и `Family` (иначе `ErrValidation`); один запрос `GROUP BY` по `target` (egress) или `source` (ingress) поверх проверок среза: `COUNT(*)`, `SUM(success)`. Возвращает группу (`target`|`site`), число адресов, строки `{key, total, ok}`.
- `RegistryBreakdownList(ctx, filter, key)`: строки проверок выбранного ключа с адресом, типом, валидатором, задержкой, деталью, временем; провалы сверху.
- Индексы `idx_checks_run` и `idx_checks_registry_cycle` уже подходят; проверить `EXPLAIN QUERY PLAN`.
### 2. HTTP API (`internal/httpapi`)
- `GET /api/v1/admin/registry/breakdown` — те же параметры, что у реестра (`q`, `last_result`, `run`, `subnet`, `direction`, `protocol`); без `direction` или `protocol` — `400`. Ответ: `{group, direction, protocol, run, addresses, rows:[{key,label,total,ok}]}`; имена площадок подставляются здесь.
- `GET /api/v1/admin/registry/breakdown/list?…&key=` — таблица `{columns, rows}`; `format=csv` — файл. Неизвестный ключ — `404`.
- Маршруты — в таблицу маршрутов (`TestRouteTableIsClassified`: admin-доступ). `docs/API.md` — описание и примеры.
### 3. Дашборд (`internal/dashboard`)
- `client.go`: `GetRegistryBreakdown`, `GetRegistryBreakdownList`, `…CSV`.
- `handlers_registry.go`: при выбранных направлении и протоколе запросить чарт и передать в шаблон; ошибка чарта показывается в самом блоке, страница не ломается. Прокси-маршруты `GET /registry/breakdown/list` и `GET /registry/breakdown/csv` (как `analytics/lists`).
- `templates/registry.html`:
- поле «Подсеть» → `<select>` (htmx-атрибуты те же, что у соседних полей);
- блок `registry_breakdown` внутри `registry_table` (обновляется вместе с таблицей): заголовок «Успешные проверки по целям» / «…по площадкам», тип проверки, строки-кнопки `an-bar-row` с серверной отрисовкой полос (без JS-библиотек), подпись и подсказка про учёт проверок;
- подключение `analytics.css`, `analytics-dialog.js`, шаблона `analytics_dialog` и нового `static/registry.js`.
- `static/registry.js` (небольшой): делегированный клик по `[data-bd-key]` (переживает htmx-замену), запрос списка с текущей строкой фильтров из адресной строки, `AnalyticsDialog.load/fill` с подсказками столбцов и ссылкой на CSV.
- Проверить, что `analytics.css` не конфликтует со стилями реестра (все классы с префиксом `an-`; токены темы общие). Если есть конфликт — вынести нужные правила в `dashboard.css`.
- Доступность: строки — `<button>`, фокус с клавиатуры, `aria-label` с числами; светлая и тёмная темы; мобильная раскладка (строка переносится, полоса остаётся).
### 4. Тесты (минимум)
- `internal/db`: один табличный тест выборок — egress по целям, ingress по площадкам, срез запуска, подсеть, статус в области, список по ключу (порядок, провалы сверху), `ErrValidation` без направления или протокола.
- `internal/httpapi`: `400` без параметров, форма ответа, список и CSV, `404` на неизвестный ключ.
- `internal/dashboard`: чарт есть при направлении + протоколе и отсутствует без них; подсеть — `<select>` с выбранным пунктом и пунктом из URL; прокси списка.
- `TestScaleSmoke6440`: чарт и список на 6440 адресов в пределах лимита.
- Правки существующих тестов, где проверялось поле ввода подсети.
## Порядок работы
1. Утвердить план и «Решения».
2. Субагент (Sonnet 5.5, Medium effort) пишет код и тесты: п.1 → п.2 → п.3 → п.4.
3. Ревью, `gofmt`, `go build ./... && go vet ./... && go test -count=1 ./...`, `EXPLAIN QUERY PLAN`.
4. Проверка на тестовом стенде `civ-test` (порты 18081/18091): пересобрать образы тега `filters`, `docker compose up -d` в `/opt/lvraid/claude/civ-teststand`, снять замеры на копии боевой БД. Боевые контейнеры не трогаем.
5. Summary в `docs/changes/…-summary.md`; обновить `README.md`, `docs/API.md`, `docs/USAGE.md`; обновить граф `/graphify . --update`.
## Риски
- Объём списка: до 6–7 тысяч адресов в диалоге, как в аналитике; для CSV — серверная выгрузка.
- Время ответа чарта на 1 млн проверок: ожидается долей секунды (один `GROUP BY` по срезу); проверить замером на копии БД стенда. При деградации — не считать чарт, пока не выбраны оба фильтра (уже предусмотрено).
- Названия площадок и целей берутся из текущей конфигурации: удалённая площадка отображается как `site-N`.
- Незавершённый запуск даёт частичные данные — как и таблица.
- Подсеть только из списка: чтобы отфильтровать произвольный CIDR, подсеть нужно добавить в настройки (ссылка рядом с полем).
@@ -0,0 +1,36 @@
# Итог: список подсетей и чарт «успешные проверки по целям / площадкам» в «Реестре»
План: [2026-10-06_13-11_registry-subnet-select-and-breakdown-chart-plan.md](2026-10-06_13-11_registry-subnet-select-and-breakdown-chart-plan.md).
Статус: код написан и проверен (gofmt, build, vet, test; страница в headless-браузере на тестовом стенде `civ-test` с копией боевой БД). Боевой стенд **не пересобирался**.
## Что изменено
- **БД** (`internal/db`): сборка `FROM`/`WHERE` фильтра вынесена из `ListRegistryPage` в `registryFilterSQL` (список и чарт используют одно условие). Новый `queries_registry_breakdown.go`: `RegistryBreakdown` (один `GROUP BY` по `target` для egress или по `source` для ingress, число адресов под фильтром) и `RegistryBreakdownList` (проверки одной строки, провалы сверху). `checks` присоединяется через `CROSS JOIN`: без статистики планировщик выбирал полный перебор `checks`.
- **API** (`internal/httpapi`): `GET /admin/registry/breakdown` и `GET /admin/registry/breakdown/list` (`format=csv`); общий разбор фильтра для трёх ручек; подписи целей (без схемы и `/`) и площадок (имя, иначе `site-N`); строки от большего числа успешных к меньшему. `400` без направления или протокола, `404` на неизвестный ключ. `docs/API.md` обновлён.
- **Дашборд** (`internal/dashboard`): «Подсеть» — `<select>` (подсеть из URL вне списка — отдельный пункт; нет настроенных подсетей — список недоступен, ссылка на `/settings`); блок чарта внутри `registry_table` (обновляется вместе с таблицей через htmx); `static/registry.js` — клик по строке открывает диалог аналитики со списком и CSV; прокси `/registry/breakdown/list` и `/registry/breakdown/csv`; подсказка, если выбрано только направление или только протокол.
- **Документы**: `API.md`, `USAGE.md`, `README.md`.
## Исправлено при ревью (по результатам просмотра страницы)
1. Полосы чарта имели разную длину: колонка с цифрами подбиралась по содержимому строки. Теперь у неё фиксированная ширина (`dashboard.css`).
2. На телефоне поля фильтров растягивались по высоте из-за встроенной базы `flex` (260/300/200 px) в колонке — отключено для `#registry-filter`; на узком экране полоса чарта переносится под подпись.
3. В диалоге столбец «Детали» (текст ошибки) был сжат в узкую полосу — задана минимальная ширина.
## Проверки
- `gofmt -l` пусто; `go vet ./...`, `go test -count=1 ./...` — все пакеты `ok`. Новые тесты: табличный `TestRegistryBreakdown…` (порядок, срез запуска, подсеть, статус в области, ingress считает проверки, список, `ErrNotFound`/`ErrValidation`, `Addresses` = `total` списка), API (400, форма, список, CSV, 404), дашборд (select подсети, чарт и прокси), `TestScaleSmoke6440`.
- `EXPLAIN QUERY PLAN`: запросы идут по `idx_checks_run` / `idx_checks_registry_cycle` и уникальному индексу `checks`; полного перебора `checks` нет.
- Тестовый стенд (копия боевой БД, 6498 адресов, 1,08 млн проверок): чарт 0,1–1,1 с в зависимости от фильтра (с запуском и без подсети ~0,7–1,1 с), список одной строки ~0,6 с. Цифры сверены с прямым SQL: `github.com` в запуске 7 — 4296 из 6490, `rxmsk` tcp — 6460 из 6492.
- В браузере (headless Chrome): выбор фильтров через htmx, чарт Egress/Ingress, клик по строке открывает диалог (3639 строк, провалы сверху), клик работает и после повторной htmx-замены таблицы, светлая и тёмная темы, ширина 390 px без горизонтальной прокрутки.
## Особенности и замечания
- Запрос диалога строится из `data-qs` блока чарта (уже проверенные фильтры, без `page`/`per_page`), а не из адресной строки.
- Стенд боевых данных не имеет проверок `tls`, а на ingress — только `icmp`, `ssh`, `tcp-22`; чарты ingress для `https` и `tls` пусты, это ожидаемо.
- Чарт добавляет к каждому обновлению таблицы запрос порядка 0,1–1,1 с (на этой копии). Если станет много — считать по кнопке.
- Время в диалоге — UTC (`Проверено (UTC)`).
- Тест `TestUpsertCheckIfOpenSetsRecordedAt` (`internal/db`, не затронут изменением) нестабилен под нагрузкой: сравнивает метки времени после паузы 5 мс. Падал в двух из ~10 полных прогонов, отдельно проходит. Стоит починить отдельным изменением.
## Выкладка
Не выполнена. Нужны пересборка и перезапуск `control-api` и `admin-dashboard`; миграций нет. Тестовый стенд `civ-test` (18081/18091) уже работает на этом коде.
@@ -0,0 +1,75 @@
# План: расширенные фильтры на странице «Аналитика»
Переносим на `/analytics` набор фильтров реестра: [итог фильтров](2026-10-06_12-12_registry-subnet-direction-protocol-filters-summary.md) и [итог подсети и чарта](2026-10-06_13-11_registry-subnet-select-and-breakdown-chart-summary.md).
## Задача
На странице «Аналитика» (один завершённый запуск) добавить фильтры, как в реестре:
1. **Подсеть** — выпадающий список; все показатели страницы считаются по адресам выбранной подсети. Это отвечает на вопрос «каково качество подсети в этом запуске».
2. **Направление** — Egress / Ingress.
3. **Протокол** — icmp, tcp, ssh, https, tls.
4. При выбранных направлении и протоколе — чарт «Успешные проверки по целям / площадкам» (как в реестре), строка кликабельна и открывает список адресов.
Запуск уже выбирается (селектор и стрелки ◀ ▶) и остаётся главным фильтром.
## Что уже есть (граф + код + итоги прошлых изменений)
- **Реестр.** Чарт, диалог со списком, CSV, шаблон `registry_breakdown`, `registry.js`, ручки `GET /admin/registry/breakdown` и `…/breakdown/list` принимают `run`, `subnet`, `direction`, `protocol` и считают по циклу адреса в запуске (`run_results.cycle_id`, `checks.run_id`). То есть для чарта аналитики **новая серверная логика не нужна**.
- **Аналитика.** Страница рисуется на клиенте из JSON `Report` (`analytics.js`): плитки, причины partial, качество данных, подсети, «Egress по целям» с вкладками типов, матрица «подсеть × цель», «Ingress по площадкам», классы ошибок, валидаторы. `analytics.Compute(Input)` читает проверки запуска одним проходом (`Input.Results`, `Input.Each`); граф связывает его с `Load`, `Analysis`, `List`. Результат кэшируется в `httpapi` на запуск (`analysisByID`, ключ — версия данных запуска и список подсетей). Списки адресов (`lists/{kind}`, CSV) режутся из того же `Analysis`.
- Расчёт полного отчёта по запуску на 6498 адресов занимает ~0,3–0,45 с (замер на стенде `civ-test`).
- Смена запуска — полная перезагрузка страницы (`location.href`), htmx на этой странице нет.
- Диалог аналитики (`analytics-dialog.js`) уже подключён и используется чартом реестра.
- Страницы связаны ссылками: подсеть в аналитике → реестр (`registry_url&subnet=`), реестр → «к аналитике запуска N».
## Решения (приняты по умолчанию — подтвердить)
1. **Набор фильтров:** запуск (есть), подсеть, направление, протокол. Статус и поиск по IP не переносим: вердикты уже открываются плитками, а поиск по адресу — задача реестра.
2. **Подсеть пересчитывает всю страницу.** Берутся адреса запуска, входящие в подсеть (вложенные подсети тоже, как в реестре), и все блоки считаются по ним: плитки, причины, качество, цели, матрица, площадки, ошибки, валидаторы. Списки адресов за плитками и CSV режутся из того же подмножества, поэтому числа совпадают с плитками.
3. **Направление и протокол не меняют расчёт вердикта и качества данных** (вердикт ставит система по всем проверкам; пересчёт по части проверок исказил бы «Качество данных»). Они делают две вещи:
- **фокус страницы:** направление скрывает блоки противоположного уровня (Egress: скрыты «Ingress по площадкам» и «Классы ошибок ingress»; Ingress: скрыты «Egress по целям», матрица, валидаторы); протокол выбирает вкладку типа в «Egress по целям» и оставляет столбец этого типа в «Ingress по площадкам»;
- **чарт** «Успешные проверки по целям / площадкам» — когда выбраны оба.
4. **Чарт — тот же, что в реестре:** шаблон, стили, `registry.js`, диалог и ручки переиспользуются; срез — выбранный запуск и подсеть. Порядок строк — от большего числа успешных к меньшему. Считаются проверки.
5. **Применение фильтров** — переходом по URL, как смена запуска: `/analytics?run=&subnet=&direction=&protocol=`. Ссылка сохраняет состояние, стрелки ◀ ▶ и «Сравнить с другим запуском» переносят подсеть, направление и протокол.
6. **Подсеть — выпадающий список** настроенных подсетей (CIDR — метка); подсеть из URL, которой нет в списке, добавляется отдельным пунктом; нет настроенных подсетей — список недоступен, ссылка на `/settings`.
7. **Незавершённые запуски** остаются недоступными (как сейчас). Страница «Сравнение запусков» вне объёма: фильтры в неё не переносим (можно отдельным изменением).
## Изменения
### 1. Аналитика (`internal/analytics`)
- `Input.Subnet` (префикс): `Compute` отфильтровывает `Results` по принадлежности адреса подсети и пропускает проверки остальных адресов в `Each`. Пустая подсеть — поведение прежнее.
- Фильтрация общая для отчёта и списков (`Analysis` строится по подмножеству, `List` режет его же).
- `Load(ctx, d, runID, subnet)`.
### 2. HTTP API (`internal/httpapi`)
- `GET /admin/analytics/runs/{id}` и `…/lists/{kind}` (JSON и CSV): параметр `subnet` (CIDR); неверный — `400`.
- Кэш: ключ — запуск + подсеть, с ограничением числа записей (вытеснение старых), чтобы перебор подсетей не раздувал память; версия данных запуска и список подсетей учитываются, как сейчас.
- `docs/API.md`: параметр и пример.
### 3. Дашборд (`internal/dashboard`)
- `handlers_analytics.go`: чтение `subnet`, `direction`, `protocol` (проверка по белому списку, как в реестре — общий разбор вынести из `parseRegistryQuery`); отчёт и списки запрашиваются с подсетью; чарт строится через уже существующий клиент `GetRegistryBreakdown` (запуск + подсеть + направление + протокол) и тот же `breakdownView`; ссылки ◀ ▶, «Сравнить» и подпись диалога несут фильтры. Прокси `analytics/lists`, `analytics/csv` передают `subnet`.
- `templates/analytics.html`: панель фильтров под выбором запуска — «Подсеть» (`<select>`), «Направление», «Протокол», «сбросить фильтры»; изменение поля переходит по URL. Блок чарта (`registry_breakdown`) над плитками. Подключить `registry.js`.
- `static/analytics.js`: фокус по направлению и протоколу (скрытие блоков, выбор вкладки типа, столбец типа в таблице площадок); ссылка «Открыть в реестре» из подсетей и матрицы переносит направление и протокол; подпись «Запуск N · подсеть …» в диалоге.
- Реестр: «к аналитике запуска N» передаёт `subnet`, `direction`, `protocol`; в обратную сторону ссылки аналитики уже передают запуск и подсеть.
- Тема, мобильная раскладка: поля фильтров по образцу реестра (в колонке без растяжения по высоте).
### 4. Тесты (минимум)
- `internal/analytics`: один тест — `Compute` с подсетью: число адресов, плитки, цели и площадки считаются по подмножеству; списки совпадают с плитками; пустая подсеть — прежний результат.
- `internal/httpapi`: `subnet` в отчёте и списке, `400` на неверный CIDR, ключ кэша не смешивает подсети.
- `internal/dashboard`: страница с фильтрами (select подсети, выбранные значения, ссылки ◀ ▶ с параметрами), чарт при направлении и протоколе и его отсутствие без них, прокси списков с `subnet`. Правка существующих тестов страницы.
## Порядок работы
1. Утвердить план и «Решения».
2. Субагент (Sonnet 5.5, Medium effort) пишет код и тесты: п.1 → п.2 → п.3 → п.4.
3. Ревью, `gofmt`, `go build ./... && go vet ./... && go test -count=1 ./...`; замеры на копии БД.
4. Проверка на тестовом стенде `civ-test` (18081/18091), в том числе в браузере (светлая и тёмная темы, телефон, клики по чарту и плиткам при выбранной подсети). Боевые контейнеры не трогаем.
5. Summary в `docs/changes/…-summary.md`; обновить `README.md`, `docs/API.md`, `docs/USAGE.md`; обновить граф `/graphify . --update`.
## Риски
- Подмножество по подсети меняет смысл плиток «всего адресов» — это нужно показывать в подписи страницы («подсеть X: N адресов из M в запуске»).
- Статистики направления и протокола не пересчитываются: при `Egress` плитки ingress остаются прежними, но блок скрыт. Это объяснить подсказкой, чтобы не путать с чартом, который считает именно выбранные проверки.
- Расхождение чарта и плиток возможно по определению: чарт считает проверки цикла запуска (включая пришедшие позже), плитки аналитики — факты тоже, но вердикты берут из запуска. В реестре разница уже описана; на аналитике нужна та же подсказка.
- Память кэша на подсети: ограничение числа записей.
- Подсеть без адресов в запуске: показать «В запуске нет адресов этой подсети» вместо пустых блоков.
@@ -0,0 +1,36 @@
# Итог: фильтры подсеть / направление / протокол и чарт на странице «Аналитика»
План: [2026-10-06_13-55_analytics-filters-plan.md](2026-10-06_13-55_analytics-filters-plan.md).
Статус: код написан и проверен (gofmt, build, vet, test; страница в headless-браузере на тестовом стенде `civ-test` с копией боевой БД). Боевой стенд **не пересобирался**.
## Что изменено
- **Аналитика** (`internal/analytics`): `Input.Subnet`; `Compute` оставляет адреса запуска внутри подсети (вложенные входят), отчёт и списки считаются по одному подмножеству; в `Report` блок `scope {subnet, run_addresses}` для подписи «N адр. из M». `Load(ctx, d, runID, subnet)` читает из БД только проверки адресов подсети.
- **БД**: `EachRunCheck` принимает список адресов (один JSON-параметр `json_each`, `nil` — все).
- **API** (`internal/httpapi`): параметр `subnet` в `GET /admin/analytics/runs/{id}` и `…/lists/{kind}` (JSON и CSV); неверный CIDR — `400`. Кэш — по запуску и подсети, не больше 16 записей (вытесняется давно не запрашивавшаяся). Сравнение запусков — без подсети. `docs/API.md` обновлён.
- **Дашборд** (`internal/dashboard`): форма фильтров под выбором запуска (подсеть — `<select>`, направление, протокол, «сбросить фильтры»), переход по URL; чарт из реестра (`registry_breakdown`, `registry.js`, диалог, ручки `registry/breakdown*`) при выбранных направлении и протоколе, срез — запуск и подсеть; общий разбор фильтров `parseSliceFilter` для реестра и аналитики; ссылки ◀ ▶, «Сравнить», аналитика↔реестр переносят фильтры; подпись диалога и страницы с подсетью.
- **Фокус по направлению и протоколу** (`analytics.js`): Egress скрывает ingress-блоки, Ingress — «Egress по целям», матрицу и валидаторы; протокол выбирает вкладку типа и столбец площадок; подсеть без адресов — пояснение вместо пустых блоков.
- **Документы**: `API.md`, `USAGE.md`, `README.md`.
## Исправлено при ревью
1. Первый расчёт новой подсети занимал ~2,4 с: читались все проверки запуска, лишние отбрасывались в Go. Теперь выборка из БД ограничена адресами подсети: **~0,5–0,6 с**, повторный запрос из кэша — ~0,3 с. Числа не изменились (подсеть `109.120.180.0/22`: 91 адрес, 10 pass — как до правки).
2. Тест на `EachRunCheck` дополнен проверкой списка адресов (пустой список читает 0 проверок).
## Проверки
- `gofmt -l` пусто; `go vet ./...`, `go test -count=1 ./...` — все пакеты `ok`; `node --check` для `analytics.js` и `registry.js`.
- Новые тесты: `TestComputeNarrowedToSubnet` (подмножество, подсеть шире запуска, списки = плитки), `TestAnalyticsSubnetFilter` (отчёт, список, CSV, 400, ключи кэша, лимит), `TestAnalyticsPageFilters` (select, ссылки, чарт есть/нет, `subnet` в запросах и прокси).
- Тестовый стенд: подсеть `109.120.180.0/22` — 91 адрес из 6498, 81 partial совпало с прямым SQL; чарт Egress https по подсети (hub.docker.com 91 из 91 …) и Ingress tcp; клик по строке и по плитке «PASS» открывает диалог с подписью подсети; направление скрывает нужные блоки; пустая подсеть даёт пояснение; ширина 390 px без горизонтальной прокрутки; ссылка реестр → аналитика несёт подсеть, направление и протокол.
## Особенности и замечания
- `run.rechecked` («перепроверено внутри запуска») при подсети остаётся по всему запуску.
- «Сравнить с другим запуском» получает фильтры в URL, но страница сравнения их пока игнорирует (вне объёма).
- Подпись среза «· подсеть X» добавлена и в реестр.
- Неверный CIDR в `/analytics?subnet=` молча отбрасывается (как в реестре); прокси списков и CSV отдают `400` от control-api.
- Тест `TestUpsertCheckIfOpenSetsRecordedAt` (`internal/db`) по-прежнему нестабилен под нагрузкой (проверка меток времени после паузы 5 мс); к изменению не относится.
## Выкладка
Не выполнена: нужны пересборка и перезапуск `control-api` и `admin-dashboard`, миграций нет. Тестовый стенд `civ-test` (18081/18091) уже работает на этом коде.