# План: удаление адресов из очереди (точечное, массовое, полная очистка) > Статус: **реализовано**. Актуальное описание — [docs/API.md](API.md#delete-apiv1adminipsip-post-apiv1adminipsdelete-post-apiv1adminipsclear), > [docs/USAGE.md](USAGE.md#удаление-адресов-из-очереди), > [docs/DASHBOARD.md](DASHBOARD.md). ## Context Сейчас у оператора есть только два способа убрать адрес из активной обработки — `POST /api/v1/admin/ips/{ip}/cancel` (переводит в `failed` с `overall_result=cancelled`, но строка и вся история проверок остаются навсегда) и косвенно `POST /api/v1/admin/ips` (принудительный повтор). Настоящего удаления — чтобы адрес вообще пропал из очереди и его больше никто не видел — нет. Нужно три операции: точечное удаление одного адреса, удаление списка адресов одной командой, полная очистка очереди — всё с поддержкой в UI дашборда. **Согласованные решения:** - **Удаление безвозвратно**: строка `ip_queue` и вся её история (`checks`, `events` с этим `ip_id`) удаляются физически, без возможности восстановления. Не путать с `cancel` — cancel сохраняет запись как историю, delete стирает её целиком. - **Можно удалить адрес в любом состоянии**, включая активно проверяемый (`assigning_fip`/`awaiting_self_check`/`checking`/`aggregating`) — удаление само выполняет то же самое, что `ForceCancel` делает для освобождения ресурсов (отвязка FIP, освобождение валидатора), и сразу переходит к физическому удалению строки, не задерживаясь в промежуточном состоянии `cancelled`. - **«Очистить список» = удалить вообще всё**, включая активные проверки — не только `queued`/терминальные. - **UI**: чекбоксы в таблице очереди + кнопка «Удалить выбранные» для массового удаления списка; отдельная кнопка «Очистить всё» для полной очистки; кнопка «Удалить» в каждой строке для точечного удаления. ## 1. База данных — `internal/db` **Ограничение схемы**, которое определяет реализацию: `checks.ip_id` (NOT NULL) и `events.ip_id` ссылаются на `ip_queue(id)` без `ON DELETE CASCADE` (`internal/db/migrations/0001_init.sql`), `validators.current_ip_id` тоже ссылается на `ip_queue(id)`. При `foreign_keys=ON` (уже включено в `db.Open`) физическое удаление строки `ip_queue` требует явно в той же транзакции: очистить `current_ip_id` у владеющего валидатора (если есть), удалить связанные `checks`, удалить связанные `events` — тот же паттерн, что уже применён в `DeleteValidator` (`internal/db/queries_validators.go`) для `owner_validator_id`. `internal/db/models.go`: новый тип результата массовой операции — ```go type DeleteIPsResult struct { Deleted []string NotFound []string } ``` `internal/db/queries_ipqueue.go`, новые функции: - `DeleteIP(ctx, ipID int64) error` — одна транзакция: освобождает владеющего валидатора (`UPDATE validators SET state=idle, current_ip_id=NULL WHERE current_ip_id=?`), удаляет `checks`/`events` по `ip_id`, удаляет строку `ip_queue`. `ErrNotFound`, если строки нет (`RowsAffected==0` на финальном DELETE). - `DeleteIPs(ctx, addresses []string) (DeleteIPsResult, error)` — одна транзакция на весь список: по каждому адресу резолвит `id` (не найден → в `NotFound`, не ошибка — тот же терпимый стиль, что у `SubmitIPs`), иначе повторяет шаги `DeleteIP` для этого `id`, добавляет адрес в `Deleted`. Используется и для точечного удаления списка, и (вызовом с полным списком адресов из `ListIPs`) для операции «очистить всё» — отдельная функция для «удалить всё» не нужна. ## 2. Оркестратор — `internal/orchestrator/orchestrator.go` DB-слой не ходит в OpenStack, поэтому отвязку floating IP для адресов с непустым `FIPID` должен делать оркестратор — по той же best-effort схеме, что уже в `ForceCancel`/`aggregateAndRelease` (лог при ошибке, не прерывает операцию: DB-состояние обязано освободиться в любом случае). ```go // DeleteIP disassociates the floating IP if attached, then permanently // removes the address and its history — differs from ForceCancel, which // keeps a cancelled record instead of deleting it. func (o *Orchestrator) DeleteIP(ctx context.Context, ipAddress string) error // DeleteIPs does the same for a specific list in one call. func (o *Orchestrator) DeleteIPs(ctx context.Context, addresses []string) (db.DeleteIPsResult, error) // ClearQueue deletes every address currently in the queue, regardless of state. func (o *Orchestrator) ClearQueue(ctx context.Context) (db.DeleteIPsResult, error) ``` `DeleteIP`: `GetIPByAddress` (обернуть `sql.ErrNoRows` в `db.ErrNotFound`, как уже сделано в `ForceCancel`) → если `FIPID != ""` → `OS.DisassociateFloatingIP` (best-effort) → `DB.DeleteIP(ctx, item.ID)`. `DeleteIPs`: по каждому адресу — `GetIPByAddress` (не найден — пропустить, `DB.DeleteIPs` сам отметит его в `NotFound`), если `FIPID != ""` — отвязать; затем один вызов `DB.DeleteIPs(ctx, addresses)`. `ClearQueue`: `DB.ListIPs` → для каждой с непустым `FIPID` отвязать → `DB.DeleteIPs(ctx, всеАдреса)`. Все три логируют событие (`o.event(...)`, `event_type`: `"ip_deleted"`/`"ips_deleted"`/`"queue_cleared"`) — но пишется общесистемное событие с `ip_id=nil` и адресами в `payload`, а не привязанное к конкретному `ip_id`: сам IP исчезнет вместе со своими событиями этим же вызовом, так что привязка к нему бессмысленна. ## 3. HTTP API — `internal/httpapi` Три новых маршрута под `/api/v1/admin/ips`, по аналогии с уже реализованными `submit`/`cancel` (`docs/API.md#управление-очередью-и-конфигурацией`): | Метод | Путь | Тело | Успех | Ошибки | |---|---|---|---|---| | DELETE | `/api/v1/admin/ips/{ip}` | — | `200 {"ok":true}` | `404` неизвестный адрес | | POST | `/api/v1/admin/ips/delete` | `{"addresses":[...]}` | `200 {"deleted":[...],"not_found":[...]}` | `400` пустой список | | POST | `/api/v1/admin/ips/clear` | — | `200 {"deleted":[...]}` | — | Выбор `POST .../delete` и `POST .../clear` вместо `DELETE` с телом — чтобы не смешивать «удалить один по пути» (чистый REST) с «удалить по списку/всё» (тело обязательно), и по аналогии с уже существующим паттерном action-эндпоинтов (`.../cancel`). Оба пути статические литералы — с существующим `.../ips/{ip}/cancel` не пересекаются (разное число сегментов), с реальными IP-адресами как значением `{ip}` тоже (слова "delete"/"clear" не бывают адресами). Новые DTO в `internal/httpapi/dto_admin.go` (snake_case, по образцу `submitIPsResponse`): ```go type deleteIPsRequest struct{ Addresses []string `json:"addresses"` } type deleteIPsResponse struct { Deleted []string `json:"deleted"` NotFound []string `json:"not_found"` } type clearQueueResponse struct{ Deleted []string `json:"deleted"` } ``` Новые хендлеры в `internal/httpapi/handlers_admin.go` (в отличие от `handleAdminSubmitIPs`, который зовёт `s.DB` напрямую, эти три идут через `s.Orch` — нужен `OS.DisassociateFloatingIP`): `handleAdminDeleteIP`, `handleAdminDeleteIPs`, `handleAdminClearQueue` — ошибки маппятся через уже существующий `writeDBError`. `routes.go`: добавить три `mux.HandleFunc(...)` рядом с существующими `/api/v1/admin/ips*`. ## 4. Dashboard — `internal/dashboard` `dto.go`: `deleteIPsResponse{Deleted, NotFound []string}`, `clearQueueResponse{Deleted []string}` — зеркало новых control-api DTO. `client.go`: `DeleteIP(ctx, ip) error`, `DeleteIPs(ctx, addresses []string) (deleteIPsResponse, error)`, `ClearQueue(ctx) (clearQueueResponse, error)` — по образцу существующих методов `CancelIP`/`SubmitIPs`. `handlers_ips.go`, новые хендлеры (все, как и остальные мутации `/ips/*`, безусловно перерисовывают `ips_table` через уже существующий `renderIPsTable` — сравнение с предыдущим состоянием не нужно, таблица после удаления просто станет короче): - `handleIPDelete` — `DELETE /ips/{ip}` → `s.CA.DeleteIP`. - `handleIPsDeleteSelected` — `POST /ips/delete` → парсит `r.ParseForm()` + `r.Form["addresses"]` (чекбоксы с одинаковым `name="addresses"` в одной форме — стандартная сериализация форм, htmx отправит все отмеченные); пустой список → баннер `ErrValidation`-стиль (`400`, «ничего не выбрано»), не идёт в API. - `handleIPsClear` — `POST /ips/clear` → `s.CA.ClearQueue`, без тела. `routes.go`: три новых маршрута рядом с существующими `/ips*`. ### Шаблоны — `templates/ips.html` - Каждая строка таблицы получает чекбокс `` в новой первой колонке; чекбокс в `` для «выбрать всё» (простой инлайновый `onclick`, без Alpine — переключает `checked` у всех `input[name=addresses]` в форме, минимальная логика, JS-фреймворк не нужен). - Вся таблица оборачивается в `
`; кнопка «Удалить выбранные» — `type="submit" hx-post="/ips/delete" hx-target="#ips-table-wrap" hx-swap="innerHTML" hx-confirm="Удалить выбранные адреса без возможности восстановления?"`. - Кнопка «Очистить всё» — отдельно (не завязана на выбор), `hx-post="/ips/clear" hx-target="#ips-table-wrap" hx-swap="innerHTML"`, с явно пугающим `hx-confirm` («Удалить ВСЕ адреса из очереди, включая те, что сейчас проверяются? Действие необратимо.») — самая опасная операция страницы, формулировка должна это отражать. - В каждой строке — кнопка «Удалить» (`.btn-danger-ghost`, `hx-delete="/ips/{{.IPAddress}}"`, свой `hx-confirm`) рядом с уже существующими «Перепроверить»/«Отменить» — для активных строк остаются обе опции (Отменить = сохранить историю как cancelled, Удалить = стереть безвозвратно вместе с историей). Никакой отдельной серверной "confirm-токен" защиты для `/clear` не вводится (как и у остальных деструктивных операций в этом API, например `DELETE /admin/config/validators/{id}`) — подтверждение только на уровне UI (`hx-confirm`), задокументировать необратимость явно в `docs/API.md`/`docs/DASHBOARD.md`. ## 5. Тесты - `internal/db/queries_dynconfig_test.go` (или новый файл рядом) — `TestDeleteIP`: удаляет queued-адрес, проверяет, что строка и её `checks`/`events` пропали; `TestDeleteIPFreesOwningValidator`: удаление адреса, которым сейчас владеет валидатор (после `ClaimNextQueued`) — валидатор становится `idle`/`current_ip_id=NULL`; `TestDeleteIPs`: смешанный список (существующий + несуществующий адрес) → `Deleted`/`NotFound` корректны. - `internal/orchestrator/orchestrator_test.go` — тест, что `DeleteIP` на адресе с привязанным (через мок) FIP вызывает `DisassociateFloatingIP` перед удалением. - `internal/httpapi/handlers_config_test.go` (или новый) — сквозной сценарий: create → assign → `DELETE /admin/ips/{ip}` во время `checking` → FIP отвязан (мок), строка пропала из `GET /admin/ips`, повторный `GET /admin/ips/{ip}` → `404`; `POST /admin/ips/delete` с несколькими адресами; `POST /admin/ips/clear` с несколькими адресами в разных состояниях → очередь пуста. - `internal/dashboard/handlers_test.go` — фейковый control-api дополняется маршрутами delete/delete-list/clear; тесты на рендер чекбоксов, на пустой выбор (баннер, не 500), на `clear`. ## 6. Документация - `docs/API.md`: новые строки в таблицу раздела «Управление очередью и конфигурацией» (рядом с `POST /admin/ips` и `/cancel`) + явное предупреждение о необратимости в отличие от `cancel`. - `docs/USAGE.md`: короткий раздел «Удаление адресов из очереди» рядом с «Принудительная остановка проверки» — с чёткой формулировкой отличия delete vs cancel. - `docs/DASHBOARD.md`: обновить описание страницы `/ips` — чекбоксы, «Удалить выбранные», «Очистить всё». ## Критичные файлы - `internal/db/models.go` (`DeleteIPsResult`), `internal/db/queries_ipqueue.go` (`DeleteIP`, `DeleteIPs`) - `internal/orchestrator/orchestrator.go` (`DeleteIP`, `DeleteIPs`, `ClearQueue`) - `internal/httpapi/dto_admin.go`, `handlers_admin.go`, `routes.go` - `internal/dashboard/dto.go`, `client.go`, `handlers_ips.go`, `routes.go`, `templates/ips.html` ## Проверка 1. `go build ./... && go test ./...`. 2. Ручной прогон на локальном стенде (`scripts/run-local-e2e.sh` + `admin-dashboard`): удалить адрес в `queued` → пропал из таблицы и БД; удалить адрес в `checking` → FIP реально отвязан (видно в моке/логах), валидатор свободен; выбрать чекбоксами несколько адресов → «Удалить выбранные» → все пропали, баннер без ошибок; «Очистить всё» на непустой очереди с активными проверками → очередь пуста, все FIP отвязаны, все валидаторы `idle`. 3. Браузерный смоук-тест страницы `/ips` (расширение Claude in Chrome, если доступно в сессии реализации — иначе тщательная curl-проверка рендера форм/чекбоксов, как в предыдущих итерациях).