diff --git a/docs/API.md b/docs/API.md
index 471ab6d..685766c 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -382,6 +382,33 @@ YAML для этой секции больше не перечитывается
Ответ: `{"ok": true}`. `404`, если адрес неизвестен. `409`, если адрес уже
в терминальном состоянии (`done`/`failed`/уже отменён) — отменять нечего.
+### `DELETE /api/v1/admin/ips/{ip}`, `POST /api/v1/admin/ips/delete`, `POST /api/v1/admin/ips/clear`
+
+**Безвозвратное удаление**, в отличие от `cancel` выше: строка `ip_queue`
+и вся её история (`checks`, `events`) стираются физически, без возможности
+восстановления. Работает из любого состояния, включая активно
+проверяемое — если Floating IP привязан, он отвязывается тем же
+best-effort способом, что и при `cancel`/обычном завершении, владеющий
+валидатор освобождается.
+
+| Метод | Путь | Тело | Успех | Ошибки |
+|---|---|---|---|---|
+| 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` удаляет ровно перечисленный список (неизвестные адреса
+идут в `not_found`, не ошибка — тот же терпимый стиль, что у `POST
+/api/v1/admin/ips`). `POST .../clear` удаляет **вообще всё**, что сейчас в
+очереди, включая адреса в процессе проверки — самая опасная операция
+этого API, используйте с осторожностью.
+
+```bash
+curl -s -X DELETE "$BASE/api/v1/admin/ips/203.0.113.10"
+curl -s -X POST "$BASE/api/v1/admin/ips/delete" -d '{"addresses":["203.0.113.10","203.0.113.11"]}'
+curl -s -X POST "$BASE/api/v1/admin/ips/clear"
+```
+
### Валидаторы: `/api/v1/admin/config/validators`
| Метод | Путь | Тело | Успех | Ошибки |
@@ -489,12 +516,18 @@ queued ──(control-api сам, без вызова API)──▶ assigning_fi
ждёт только `egress_complete`, ни одна площадка не требуется. Подробнее —
[USAGE.md](USAGE.md#управление-площадками-проберами).
-Два дополнительных перехода, оба инициируются оператором через
-`/api/v1/admin/ips`, а не самим оркестратором:
+Три дополнительных перехода, все инициируются оператором через
+`/api/v1/admin/ips*`, а не самим оркестратором:
- **любое нетерминальное состояние → `failed` (`overall_result:
"cancelled"`)** — `POST /api/v1/admin/ips/{ip}/cancel`;
- **`done`/`failed` → `queued` (новая попытка)** — `POST
- /api/v1/admin/ips` с уже завершённым адресом в списке.
+ /api/v1/admin/ips` с уже завершённым адресом в списке;
+- **любое состояние → адрес физически исчезает из очереди**, вместе со
+ всей историей — `DELETE /api/v1/admin/ips/{ip}`, `POST
+ /api/v1/admin/ips/delete`, `POST /api/v1/admin/ips/clear` (см.
+ [выше](#delete-apiv1adminipsip-post-apiv1adminipsdelete-post-apiv1adminipsclear)).
+ Не путать с cancel — cancel сохраняет запись как историю (`failed`/
+ `cancelled`), delete стирает её целиком без возможности восстановления.
## Сквозной пример работы (curl)
diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md
index 65ce227..c12d4e2 100644
--- a/docs/DASHBOARD.md
+++ b/docs/DASHBOARD.md
@@ -49,7 +49,7 @@ admin-dashboard -config /etc/cloud-ip-validator/admin-dashboard.yaml
| Страница | Назначение |
|---|---|
| `/overview` | Сводная статистика: счётчики по состояниям, «текущая проверка» (live-снимок всех IP не в терминальном состоянии) и «последние N завершённых» (по умолчанию 20, `overview.last_completed_count`) с разбивкой pass/partial/fail/cancelled. Обновляется каждые `overview.poll_interval_seconds` секунд без перезагрузки страницы. |
-| `/ips` | Полная очередь. Форма сверху принимает список адресов (по одному на строке или через запятую) и отправляет их в `POST /api/v1/admin/ips` — **один и тот же вызов** добавляет новые адреса и принудительно перезапускает уже завершённые (см. ниже). У каждого адреса — кнопка «Перепроверить» (для `done`/`failed`) или «Отменить» (для активных состояний). |
+| `/ips` | Полная очередь. Форма сверху принимает список адресов (по одному на строке или через запятую) и отправляет их в `POST /api/v1/admin/ips` — **один и тот же вызов** добавляет новые адреса и принудительно перезапускает уже завершённые (см. ниже). У каждого адреса — кнопка «Перепроверить» (для `done`/`failed`) или «Отменить» (для активных состояний), и всегда — «Удалить» (безвозвратно, в отличие от «Отменить», см. ниже). Чекбоксы у строк + кнопка «Удалить выбранные» удаляют список одним вызовом; «Очистить всё» удаляет вообще всё, включая активные проверки — обе операции требуют явного подтверждения. |
| `/ips/{ip}` | Детали одного адреса: все проверки текущей попытки и вся история событий. |
| `/validators` | Список валидаторов + создание/изменение `os_port_id`/удаление. |
| `/sites` | Три фиксированных слота площадок (1/2/3) — назначить/сменить/освободить `site_id`. |
@@ -82,6 +82,20 @@ admin-dashboard -config /etc/cloud-ip-validator/admin-dashboard.yaml
(дашборд честно показывает это в таблице, а не делает вид, что запрос
ничего не значил).
+### Удаление адресов — безвозвратно, в отличие от «Отменить»
+
+«Отменить» (`POST .../cancel`) останавливает проверку, но сохраняет
+адрес и его историю как `failed`/`cancelled` — он остаётся виден в
+очереди. «Удалить» (кнопка в строке, «Удалить выбранные» по чекбоксам,
+«Очистить всё») стирает строку и всю её историю проверок/событий
+физически, без возможности восстановления — работает из любого
+состояния, включая активно проверяемое (Floating IP отвязывается,
+валидатор освобождается). Все три операции удаления в UI защищены
+`hx-confirm` с формулировкой, отражающей необратимость — «Очистить всё»
+предупреждает отдельно, так как затрагивает и активные проверки. Подробнее
+— [API.md](API.md#delete-apiv1adminipsip-post-apiv1adminipsdelete-post-apiv1adminipsclear)
+и [USAGE.md](USAGE.md#удаление-адресов-из-очереди).
+
## Конфигурация
См. `configs/admin-dashboard.example.yaml`. Ключевые поля:
diff --git a/docs/PLAN_DELETE_IPS.md b/docs/PLAN_DELETE_IPS.md
new file mode 100644
index 0000000..f5c59fb
--- /dev/null
+++ b/docs/PLAN_DELETE_IPS.md
@@ -0,0 +1,252 @@
+# План: удаление адресов из очереди (точечное, массовое, полная очистка)
+
+> Статус: **реализовано**. Актуальное описание — [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-фреймворк
+ не нужен).
+- Вся таблица оборачивается в `