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

+83 -6
View File
@@ -671,8 +671,8 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
### `GET /api/v1/admin/registry`
Список адресов реестра с краткой сводкой по каждому. Без параметров — все адреса одним массивом; **с `limit`** (`1`…`1000`) —
постраничный конверт `{"items": [...], "total": N, "limit": L, "offset": O}`, параметры `offset`, `q` (подстрока адреса) и
`last_result` (`pass`/`partial`/`fail`/`cancelled`). Страница и фильтры применяются в SQL до расчёта сводки, поэтому
постраничный конверт `{"items": [...], "total": N, "limit": L, "offset": O, "run": R}`, параметры `offset`, `q` (подстрока адреса) и
`last_result` (`pass`/`partial`/`fail`/`cancelled`); остальные фильтры — ниже. Страница и фильтры применяются в SQL до расчёта сводки, поэтому
реестр из тысяч адресов отдаётся за доли секунды.
```json
@@ -714,9 +714,77 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
результаты, поэтому при неполном наборе вердикт может быть хуже, чем «`ok` из
`total`».
Фильтры постраничного режима (только вместе с `limit`): `run` — только адреса,
у которых есть результат в этом запуске (см. [«Аналитика запусков»](#аналитика-запусков)); `subnet` — только
адреса внутри подсети (CIDR, например `203.0.113.0/24`). Неверный `run` или `subnet` — `400`.
Фильтры постраничного режима (только вместе с `limit`), комбинируются через И:
| Параметр | Значения | Смысл |
|----------|----------|-------|
| `run` | id запуска | Только адреса запуска (см. [«Аналитика запусков»](#аналитика-запусков)); открытый (идущий) запуск тоже допустим, данные в нём частичные. Неизвестный запуск — пустой список. |
| `subnet` | CIDR, например `203.0.113.0/24` | Только адреса внутри подсети. |
| `direction` | `egress`, `ingress` | Только проверки этого направления. |
| `protocol` | `icmp`, `tcp`, `ssh`, `https`, `tls` | Только проверки этого семейства (`tcp` = `tcp-22`, `tcp-443` и т. д.). `tls` бывает только на входе, поэтому `direction=egress&protocol=tls` всегда даёт пустой список. |
Неверные `run`, `subnet`, `direction`, `protocol` и `last_result` — `400`.
**Запуск как срез данных.** Без `run` поля ответа считаются по последнему
циклу адреса. С `run` — по циклу адреса **в этом запуске**: `last_cycle_id` —
цикл запуска, `last_result` — вердикт запуска (как на странице «Аналитика»),
`egress`/`ingress` — проверки этого цикла. Если проверки запуска уже удалены
(очистка истории, `history_retention_cycles`), адрес остаётся в списке, а
уровни пусты (`total: 0`). Значение `run` возвращается в конверте (`0` — срез
не задан).
**Направление и протокол** сужают область проверок: в `egress`/`ingress` и
`by_type` попадают только подходящие проверки, а адрес без таких проверок из
списка исключается. `last_result`-фильтр при этом считается по проверкам
области, а не по вердикту: все успешны — `pass`, ни одной — `fail`, иначе
`partial` (вердикт учитывает и недостающие результаты, поэтому может
отличаться); `cancelled` вместе с `direction`/`protocol` всегда даёт пустой
список. Поле `last_result` в самих строках остаётся вердиктом (запуска или
последнего цикла).
Пример: адреса подсети, у которых в запуске 3 все входящие проверки `https`
неуспешны:
```
GET /api/v1/admin/registry?limit=50&run=3&subnet=203.0.113.0/24&direction=ingress&protocol=https&last_result=fail
```
### `GET /api/v1/admin/registry/breakdown`
Успешные проверки по целям (egress) или площадкам (ingress) — данные чарта над таблицей реестра. Параметры те же, что
у `GET /api/v1/admin/registry` (`q`, `last_result`, `run`, `subnet`, `direction`, `protocol`; `limit` и `offset` не нужны), но
`direction` и `protocol` **обязательны** — без любого из них `400`. Срез и набор адресов тоже те же, что у списка: все адреса под фильтром (не
страница), цикл адреса в запуске `run` или его последний цикл, проверки выбранных направления и протокола.
```json
{
"group": "site", "direction": "ingress", "protocol": "tcp", "run": 3, "addresses": 6440,
"rows": [
{"key": "inbound-site-2", "label": "rxspb", "total": 12880, "ok": 12790},
{"key": "inbound-site-1", "label": "rxmsk", "total": 12880, "ok": 12611}
]
}
```
`group` — `target` (egress, группировка по `checks.target`) или `site` (ingress, по `checks.source`); `addresses` совпадает с `total`
списка при тех же фильтрах. `key` — исходное значение (его принимает `…/list?key=`), `label` — подпись: у цели адрес без схемы и
завершающего `/` (`repo.almalinux.org/almalinux`), у площадки её имя из настройки (`site-N`, если площадка уже удалена). Считаются
**проверки**, а не адреса: `total` — записанные проверки, `ok` — успешные; у `tcp` и `tls` на ingress у адреса бывает по проверке на
каждую площадку и порт. Строки идут от большего `ok` к меньшему, при равенстве — по `label`. Нет проверок (например, `egress` + `tls`) —
`rows: []`.
### `GET /api/v1/admin/registry/breakdown/list`
Проверки одной строки чарта: те же параметры плюс `key` (обязателен, значение `key` из строки чарта). Ответ — таблица
`{"columns": [...], "rows": [[...]]}`; сначала провалы, затем в порядке реестра. Столбцы: `Адрес`, `Результат` (`успешно`/`провал`),
`Тип проверки` (`https`, `tcp-22`…), `Цель` (egress) или `Площадка` (ingress), `Валидатор` (только egress), `Задержка, мс`, `Детали`
(ошибка), `Проверено (UTC)`. С `format=csv` — тот же список файлом (`registry_<направление>_<протокол>_<ключ>.csv`, UTF-8 с BOM).
`404` — у ключа нет проверок в этом срезе; `400` — нет `key`, `direction` или `protocol`.
```
GET /api/v1/admin/registry/breakdown?run=3&direction=egress&protocol=https
GET /api/v1/admin/registry/breakdown/list?run=3&direction=egress&protocol=https&key=https://repo.almalinux.org/almalinux/&format=csv
```
### `GET /api/v1/admin/registry/{ip}`
@@ -1047,10 +1115,18 @@ curl -s "$BASE/api/v1/admin/ips/203.0.113.10" | python3 -m json.tool
Все показатели страницы по одному **завершённому** запуску; открытый запуск — `409`, неизвестный — `404`.
Считаются проверки последнего цикла каждого адреса в запуске, в том числе пришедшие позже вердикта (как факты).
Результат кэшируется, пока данные запуска и список подсетей не менялись.
Результат кэшируется, пока данные запуска и список подсетей не менялись; кэш ведётся по паре «запуск + подсеть» и хранит не больше 16 записей (вытесняется давно не запрашивавшаяся).
Необязательный параметр `subnet` (CIDR, например `203.0.113.0/24`) пересчитывает все блоки по адресам запуска внутри подсети
(вложенные подсети тоже; биты хоста маскируются). Неверный CIDR — `400`. В ответе тогда есть блок `scope`.
```
GET /api/v1/admin/analytics/runs/7?subnet=203.0.113.0/24
```
| Блок | Содержимое |
|---|---|
| `scope` | только с `subnet`: `subnet` (CIDR в каноничной записи) и `run_addresses` (все адреса запуска без `cancelled`; `summary.addresses` — адреса подсети). `run.rechecked` остаётся по всему запуску |
| `run` | `id`, `kind`, `state`, `started_at`, `finalized_at`, `duration_seconds`, `rechecked` (адресов с несколькими циклами в запуске) |
| `summary` | `addresses`, `pass`, `partial`, `fail`, `cancelled`; `egress_ok`, `ingress_ok` (адреса, у которых все записанные проверки уровня успешны); `egress_https_any_failed` и `egress_https_all_failed` (хотя бы одна / все https-проверки провалены), `egress_https_all_targets_failed` (все цели полного набора); `ingress_ssh_any_failed`, `ingress_ssh_all_failed`; `addresses_per_minute` |
| `reasons` | причины `partial`, каждый адрес один раз: «Только egress», «Ingress и egress», «Egress и неполный набор», «Ingress, egress и неполный набор», «Только неполный набор», «Только ingress»; нулевые не выдаются |
@@ -1067,6 +1143,7 @@ curl -s "$BASE/api/v1/admin/ips/203.0.113.10" | python3 -m json.tool
### `GET /api/v1/admin/analytics/runs/{id}/lists/{kind}`
Таблица адресов за показателем или классом ошибки: `{"kind", "class", "columns": [...], "rows": [[...]]}`.
С `?subnet=<CIDR>` — только адреса этой подсети, так что строки совпадают с числами отчёта той же подсети; неверный CIDR — `400` (то же для CSV).
`kind`: `verdict_pass`, `verdict_partial`, `verdict_fail`, `egress_https_any`, `egress_https_all`, `ingress_ssh_any`, `ingress_ssh_all` или `error` (с `?class=SSH: таймаут`;
без класса и неизвестный `kind` — `404`). С `?format=csv` — файл CSV (UTF-8 с BOM, `Content-Disposition: attachment`,
имя вида `ingress_ssh_all_run1.csv`). Для `verdict_*` — адреса запуска с этим вердиктом (без `cancelled`, по числовому порядку; число строк равно `summary.pass`/`partial`/`fail`): адрес, подсеть, валидатор (по https-проверкам, «—», если их нет), `Egress` и `Ingress` («успешно из всех», «—» без проверок), «Проверок в цикле» (записано из ожидаемых) и у `partial` ещё «Причина» (как в блоке `reasons`). Для `error` строка — одна проваленная проверка: адрес, подсеть, площадка,
+60
View File
@@ -412,6 +412,51 @@ curl -s http://<control-api>:8080/api/v1/admin/registry/203.0.113.10 | python3 -
видно по счётчикам (подсказка при наведении). Те же данные — в
`GET /api/v1/admin/registry` ([API.md](API.md#get-apiv1adminregistry)).
**Фильтры реестра.** Помимо поиска по IP, статуса и числа строк на странице,
доступны (все сохраняются в адресной строке и при листании):
- **Запуск** — задача сканирования из накопленной истории (список тот же, что
на «Аналитике»; идущие запуски помечены «идёт»). Выбранный запуск оставляет
только его адреса, а результат и статус берутся по циклу адреса *в этом
запуске* (статус — вердикт запуска). «Последний цикл» — поведение по умолчанию.
- **Подсеть** — выпадающий список настроенных подсетей (с метками); показываются
адреса реестра, входящие в подсеть. Подсеть из ссылки аналитики, которой нет в
списке, добавляется отдельным пунктом. Если подсети не настроены, список
недоступен — рядом ссылка на `/settings`, где их можно добавить. Через API
(`subnet=`) по-прежнему принимается любой CIDR.
- **Направление** — `Egress` или `Ingress`.
- **Протокол** — `icmp`, `tcp` (все порты), `ssh`, `https`, `tls`. `tls` — это
TLS-хендшейк пробера на 443, он бывает только на входе: «Egress + tls» всегда
пуст.
Направление и протокол оставляют только такие проверки цикла: адрес должен
иметь хотя бы одну, а счётчики «N из M» в строке считаются только по ним.
Вместе со статусом он считается по этим проверкам (все успешны — `pass`, ни
одной — `fail`, иначе `partial`), поэтому может отличаться от общего
вердикта. `cancelled` вместе с направлением или протоколом ничего не находит.
Если история циклов обрезана (`history_retention_cycles`), у старого запуска
проверок может не остаться — уровни покажут «—».
**Чарт «Успешные проверки по целям / площадкам».** Когда выбраны и направление, и
протокол, над таблицей появляется чарт по проверкам выбранного типа в цикле среза
(цикл адреса в выбранном запуске либо последний):
- **Egress** — одна строка на цель (`github.com`, `hub.docker.com`…);
- **Ingress** — одна строка на площадку пробера.
В строке — «успешно из всего» и доля, например `4 296 из 6 490 · 66,2%`; строки идут
от большего числа успешных проверок к меньшему. Чарт считается по всем адресам под
текущим фильтром (число — в строке «Найдено адресов»), а не по одной странице.
Считаются именно проверки: у `tcp` и `tls` на ingress у адреса может быть по проверке
на каждый порт (22, 443) для каждой площадки. Сочетание без проверок (например,
`Egress` + `tls`) показывает «Для этого сочетания проверок нет».
Строка чарта открывает список адресов с этой целью или площадкой: провалы сверху,
столбцы «Результат», «Тип проверки», «Цель»/«Площадка», «Валидатор» (egress),
«Задержка», «Детали» (ошибка) и «Проверено»; есть «Копировать» и «Скачать CSV».
API: [`GET /admin/registry/breakdown`](API.md#get-apiv1adminregistrybreakdown) и
`…/breakdown/list`.
**Глубина хранения.** Чтобы история не росла бесконечно на адресах,
которые перепроверяют очень часто, можно ограничить, сколько последних
циклов проверки хранить на каждый адрес — `history_retention_cycles` на
@@ -428,6 +473,21 @@ curl -s http://<control-api>:8080/api/v1/admin/registry/203.0.113.10 | python3 -
`partial`, подсети, провалы по целям, ingress по площадкам, классы ошибок, валидаторы и качество данных. Данные
других запусков на странице не участвуют, поэтому результаты разных прогонов не пересекаются.
**Фильтры страницы.** Под выбором запуска — те же фильтры, что в реестре; значения лежат в адресе
(`/analytics?run=&subnet=&direction=&protocol=`), поэтому ссылку можно сохранить; стрелки ◀ ▶ и ссылки в реестр их переносят:
- **Подсеть** — выпадающий список настроенных подсетей. Вся страница пересчитывается по адресам запуска, входящим в
подсеть (вложенные тоже): плитки, причины `partial`, качество данных, цели, матрица, площадки, ошибки, валидаторы;
списки за плитками и CSV режутся из того же набора. В подписи — «подсеть X: N адр. из M в запуске». Подсеть без
адресов в запуске показывает пояснение. Расчёт новой подсети занимает до секунды, повторный — мгновенно (кэш до 16
записей).
- **Направление** и **протокол** не пересчитывают вердикты и «Качество данных», а задают фокус: Egress скрывает блоки
ingress, Ingress — блоки egress и валидаторов; протокол выбирает вкладку типа в «Egress по целям» и столбец типа в
«Ingress по площадкам».
- Когда выбраны **оба**, над плитками появляется чарт «Успешные проверки по целям / площадкам» — тот же, что в реестре
(чарт считает проверки цикла запуска, в том числе пришедшие после вердикта, поэтому его числа могут расходиться
с плитками, которые берут вердикты). Строка чарта открывает список адресов с CSV.
**Что такое запуск.** Запуск открывается, когда адрес попадает в пустую (или полностью обработанную) очередь, а
скан автоцикла помечает его как `авто`. Пока он открыт, в него входят все добавленные и перепроверяемые адреса.
Когда у всех адресов запуска есть итог, запуск завершается и появляется в списке. Перепроверка после этого
@@ -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) уже работает на этом коде.