Files
cloud-ip-validator/docs/changes/2026-10-03_16-24_registry-egress-ingress-levels-plan.md
T
ayurishchevandClaude Sonnet 5.5 864208238f Show egress/ingress levels in the registry; freeze checks at the verdict
Registry: the "last result" column now also shows, per level (egress,
ingress), how many of the recorded checks of the latest cycle succeeded, split
by check family (tcp-22 and tcp-443 are both "tcp"). One grouped query per
chunk of addresses; new fields last_cycle_id, egress, ingress in
GET /admin/registry; the dashboard renders them under the verdict.

Verdict integrity (migration 0010):
- the prober is handed an address once per site and attempt, not on every
  poll, so results are no longer overwritten by later probe rounds;
- UpsertCheckIfOpen refuses writes once the address is aggregating or has its
  verdict, or for an older attempt; senders get {"ok":true,"ignored":N} and a
  result_dropped event is recorded;
- the checking window counts from checking_started_at, not from assigned_at;
- checks.recorded_at (server clock) and checks.after_verdict (flag for rows
  written after the verdict in existing data);
- the verdict rule is a pure function (computeVerdict) and the aggregated
  event carries the egress/ingress check counts.

Rebuilt bin/control-api and bin/admin-dashboard to match. Plans and summaries
are in docs/changes; README, API, USAGE, DASHBOARD and DIAGRAMS are updated.

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

83 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: уровни Egress / Ingress в таблице «Реестр»
## Контекст
Колонка «Последний результат» в «Реестре» показывает одну пилюлю общего вердикта (pass / partial / fail / cancelled). Для аналитики этого мало: не видно, где именно сбой — на выходе (egress) или на входе (ingress) — и какой тип проверки упал.
Нужно разделить результат на два уровня и показать «успешно из всего» (например, «5 из 5») по каждому уровню, с разбивкой по типу проверки (icmp, ssh, tcp, https, tls и новые типы, когда появятся).
Решения пользователя:
- Пилюля общего вердикта остаётся, уровни добавляются под ней. Фильтр по вердикту не меняется.
- «Всего» = число записанных проверок последнего цикла (не ожидаемое по настройкам).
## Что уже есть (по графу и коду)
- Проверки лежат в `checks` (`internal/db/migrations/0007_ip_registry.sql:39-65`). Уровень задаёт `source`: `egress` или `inbound-site-N` (`internal/db/models.go:39,68-72`). Тип — в `check_type`: `https`, `icmp`, `ssh`, `tcp-<порт>`, `tls-<порт>`.
- Индекс `idx_checks_registry_cycle(registry_id, cycle_id)` уже есть. Миграция не нужна.
- Цепочка данных: `fillRegistrySummary` (`internal/db/queries_registry.go:199`) → `registrySummaryToDTO` (`internal/httpapi/handlers_registry.go:75`) → `registryDTO` (`internal/httpapi/dto_admin.go:87`) → клиент дашборда → `registryItem` (`internal/dashboard/dto.go:258`) → шаблон `internal/dashboard/templates/registry.html` (`registry_table`, строки 69–101).
- Дашборд ходит в БД только через HTTP control-api. Менять нужно оба слоя.
## Правила расчёта
- Цикл: максимальный `cycle_id` в `checks` для адреса (так же, как сейчас считается `LastCheckedAt`).
- Уровень: `source = 'egress'` → Egress; `source LIKE 'inbound-site-%'` → Ingress. Прочее игнорируется.
- Тип: часть `check_type` до первого `-` (`tcp-22`, `tcp-443` → `tcp`; `tls-443` → `tls`; `ssh`, `icmp`, `https` без изменений). Новый тип попадает в разбивку сам, без правок кода.
- Ingress: считаются проверки всех площадок вместе (площадок может быть любое число).
- Правило разбора живёт в одном месте: две функции в `internal/db/models.go` рядом с `InboundSource`: `CheckLevel(source)` и `CheckFamily(checkType)`.
- Группировка делается в Go, не в SQL: SQL отдаёт `GROUP BY registry_id, source, check_type`, и это не больше ~30 строк на адрес.
## Изменения
### 1. БД (`internal/db`)
- `models.go`: типы `TypeStat{Type, Total, OK}` и `LevelResult{Total, OK, ByType []TypeStat}` (по типам — в алфавитном порядке). Функции `CheckLevel`, `CheckFamily`.
- `queries_registry.go`: в `RegistrySummary` добавить `LastCycleID`, `Egress`, `Ingress`. Один пакетный запрос на страницу вместо запроса на каждый адрес:
```sql
SELECT c.registry_id, c.source, c.check_type, COUNT(*), SUM(c.success)
FROM checks c
JOIN (SELECT registry_id, MAX(cycle_id) AS cid FROM checks
WHERE registry_id IN (…) GROUP BY registry_id) m
ON m.registry_id = c.registry_id AND m.cid = c.cycle_id
GROUP BY c.registry_id, c.source, c.check_type
```
Новая функция заполняет сводки списком адресов (порциями по 500). Её вызывают `ListRegistryPage`, `ListRegistry` и `GetRegistryByAddress`. Это заодно убирает N+1 для новых полей.
- `lastResultCond` и `fillRegistrySummary` (вердикт) не меняются.
### 2. HTTP API (`internal/httpapi`)
- `dto_admin.go`: в `registryDTO` добавить `last_cycle_id`, `egress`, `ingress` как `{total, ok, by_type: [{type, total, ok}]}`. Изменение только добавляет поля, старые клиенты не ломаются.
- `handlers_registry.go`: `registrySummaryToDTO` заполняет новые поля.
- `docs/API.md` (раздел «Реестр адресов», ~645–668): новые поля и пример JSON.
### 3. Дашборд (`internal/dashboard`)
- `dto.go`: `registryItem` получает те же поля.
- `templates/registry.html`: в ячейке «Последний результат» сверху пилюля вердикта, ниже две строки:
`Egress 5 из 5` и `Ingress 3 из 4`, под каждой чипы по типам: `https 3 из 3`, `icmp 2 из 2`. Цвет: все успешны — зелёный, ни одной — красный, иначе жёлтый. Нет проверок — «—». В `title` ячейки: «по записанным проверкам цикла N; вердикт учитывает недостающие результаты». Сохранить `data-label` для мобильной раскладки.
- `static/dashboard.css` (~417–423): стили строки уровня и чипа по образцу `pill-*`.
- Страница деталей `registry_detail.html` не меняется (вне объёма).
### 4. Тесты
- `internal/db/queries_registry_test.go`: новые тесты — разбивка egress/ingress по типам; `tcp-22` и `tcp-443` сливаются в `tcp`; `tls`, `ssh`; новый тип (например `dns`) появляется в разбивке; адрес без проверок даёт нули; берётся последний цикл; работает после удаления строки очереди; несколько адресов в одном пакетном запросе. Добавить хелпер с ingress-проверками рядом с `finishWithChecks` (`queries_scale_test.go:24`).
- `internal/httpapi/handlers_scale_test.go` (`TestAdminRegistryPaginationFiltersAndCompat`): проверить новые поля в ответе.
- `internal/dashboard/handlers_test.go` (`TestRegistryPageAndDetail`): фикстура с уровнями, проверка вывода «5 из 5» и имён типов.
- `TestScaleSmoke6440` (`queries_scale_test.go:356`): добавить ingress-проверки в засев, лимит 10 с сохраняется.
## Порядок работы (по правилам проекта)
1. Скопировать этот план в `docs/changes/2026-10-03_<ЧЧ-ММ>_registry-egress-ingress-levels-plan.md`.
2. Реализовать п.1 → п.2 → п.3 → п.4.
3. Написать `docs/changes/…-summary.md` (что изменено).
4. Обновить `README.md` и `docs/API.md`/`docs/USAGE.md`, где описан реестр.
5. Обновить граф: `/graphify . --update`.
## Проверка
- `go build ./... && go vet ./... && go test ./...`
- `EXPLAIN QUERY PLAN` пакетного запроса: должен идти по `idx_checks_registry_cycle`, без полного перебора `checks`.
- Локальный стенд (`scripts/run-local-e2e.sh`, `docs/LOCAL_E2E.md`): прогнать цикл, открыть `/registry`, сверить «N из M» по уровням и типам с таблицей на странице деталей `/registry/<ip>`.
- Страница на 6440 адресов отвечает за время порядка прежних 0,12 с (допустим рост в разы, но не секунды).
## Ограничения и риски
- «N из M» считается по записанным проверкам. Если результатов не хватает, вердикт может быть хуже, чем «N из M» (например, все записанные успешны, а вердикт `partial`). Это показано подсказкой.
- Пока идёт цикл, счётчики отражают частично пришедшие проверки текущего цикла. Состояние цикла видно в колонке состояния.
- Фильтра и сортировки по уровням нет: сейчас вне объёма. Если понадобятся для аналитики, это отдельное изменение.