# План: `cmd/admin-dashboard` — веб-панель администратора > Статус: **реализовано**. Актуальное описание — [docs/DASHBOARD.md](DASHBOARD.md). ## Context Единственный способ управлять `control-api` (очередь IP, валидаторы, площадки, цели проверки) — HTTP API через `curl` (см. `docs/API.md`). API уже покрывает весь необходимый функционал (`POST /api/v1/admin/ips` для постановки/принудительного повтора, `POST /api/v1/admin/ips/{ip}/cancel` для остановки, `/api/v1/admin/config/{validators,sites,targets,check-types}` для CRUD), но curl неудобен для повседневного оперирования и не даёт наглядной картины состояния очереди. Нужен браузерный admin dashboard — графический доступ ко всей этой функциональности: сводная статистика (текущая проверка / итог последней завершённой), управление конфигурацией и принудительные операции над очередью — без YAML и без перезапуска `control-api`. **Согласованные решения:** - **Без сборки фронтенда.** Server-rendered `html/template` + htmx (частичные AJAX-обновления) + Alpine.js (точечная клиентская интерактивность) + Pico.css classless (минимализм на голой семантической разметке). Все три библиотеки вендорятся как статические файлы и встраиваются через `//go:embed` — без CDN во время выполнения (принцип проекта — полностью автономный бинарник, работает офлайн). - **Никакого нового backend-состояния.** «Текущая проверка» — live-снимок IP не в терминальном состоянии. «Последняя завершённая» — последние N (по умолчанию 20, настраивается) по `AggregatedAt` desc среди `done`/`failed`, с разбивкой по `OverallResult`. Оба вычисляются на каждый запрос из `GET /admin/status` + `GET /admin/ips` — никакого понятия «запуска»/«батча» в `control-api` не добавляется. - **Без аутентификации** — как и сам API; доступ ограничивается сетью/firewall. - **Отдельный 4-й бинарник** (`cmd/admin-dashboard`), не новые маршруты внутри `control-api`. Ходит в `control-api` только через существующий HTTP admin API (`internal/apiclient.Client`). Рендеринг на сервере — браузер не видит JSON `control-api` напрямую, CORS/reverse-proxy не нужны. - Малые допущения: без пагинации очереди (текущий масштаб — десятки адресов); баннер ошибок различает 4xx (жёлтый) и 5xx/транспортные (красный); порт дашборда по умолчанию `:8090`. ## 1. Структура файлов ``` cmd/admin-dashboard/main.go internal/dashboard/ server.go, routes.go, client.go, dto.go, render.go, embed.go handlers_overview.go, handlers_ips.go, handlers_validators.go, handlers_sites.go, handlers_targets.go, handlers_checktypes.go templates/ (layout, overview[+fragment], ips[+table], ip_detail, validators[+table+row], sites[+table], targets[+table+row], checktypes[+table+row], error_banner) static/vendor/{htmx.min.js,alpine.min.js,pico.classless.min.css,VENDOR.md} static/dashboard.css configs/admin-dashboard.example.yaml deploy/systemd/admin-dashboard.service docs/DASHBOARD.md ``` Таблица `ips` перерисовывается целиком при любой мутации (`POST /admin/ips` может завести новые строки и поменять `sequence`). `validators`/`sites`/`targets`/`check-types` — точечный swap одной ``. ## 2. Маршруты дашборда | Метод | Путь | Вызов к control-api | |---|---|---| | GET | `/` | редирект на `/overview` | | GET | `/overview`, `/overview/fragment` | `GET /admin/status`, `GET /admin/ips` | | GET | `/ips`, `/ips/{ip}` | `GET /admin/ips`, `GET /admin/ips/{ip}` | | POST | `/ips` | `POST /admin/ips` | | POST | `/ips/{ip}/recheck` | `POST /admin/ips` `{"addresses":[ip]}` | | POST | `/ips/{ip}/cancel` | `POST /admin/ips/{ip}/cancel` | | GET/POST `/validators`, PUT/DELETE `/validators/{id}` | `.../config/validators[/{id}]` | | GET `/sites`, PUT/DELETE `/sites/{index}` | `.../config/sites[/{index}]` | | GET/POST `/targets`, PUT/DELETE `/targets/{group}` | `.../config/targets[/{group}]` | | GET/POST `/check-types`, PUT/DELETE `/check-types/{name}` | `.../config/check-types[/{name}]` | | GET | `/static/*` | embed.FS | `overview/fragment` — `hx-trigger="every Ns"` (из конфига), без фонового тикера на сервере. `POST /targets`/`/check-types` — перевод «форма с именем» → `PUT .../{name}` (control-api там upsert). ## 3. Ошибки control-api `client.go`: не-2xx → `*apiErr{Status, Message}`. Хендлеры не отдают 500 — рендерят страницу/фрагмент + out-of-band `error_banner.html` (`hx-swap-oob="true"`, `id="error-banner"` в `layout.html`); статус ответа дашборда = статус control-api (502 при транспортной ошибке). Баннер жёлтый для 4xx, красный для 5xx/транспортных. ## 4. Конфигурация `internal/config/config.go`, секция `AdminDashboard{Server, ControlAPI{ BaseURL, TimeoutSeconds}, Overview{LastCompletedCount, PollIntervalSeconds}}` + `LoadAdminDashboard` (defaults: `:8090`, timeout 10s, N=20, poll=5s; `BaseURL` обязателен). `configs/admin-dashboard.example.yaml`, `deploy/systemd/admin-dashboard.service` (без CAP_NET_RAW/EnvironmentFile). ## 5. DTO и клиент `internal/dashboard/dto.go` — свои wire-структуры (не импортируют приватные DTO `internal/httpapi`, тот же паттерн, что `internal/probercore`): snake_case-структуры дословно повторяют `internal/httpapi/dto_admin.go`; для `GET /admin/ips[/{ip}]`/`GET /admin/validators` — зеркала untagged PascalCase `db.IPQueueItem`/`db.Validator`/`db.Check`/`db.Event`. `client.go` — обёртка над `apiclient.Client`: `Status`, `ListIPs`, `GetIP`, `SubmitIPs`, `CancelIP`, CRUD-методы для validators/sites/target-groups/ check-types. `handlers_overview.go` — чистые функции: `currentlyChecking`, `lastCompleted(items, n)`, `resultBreakdown`. ## 6. Вендоринг статики Скачать один раз, закоммитить, задокументировать в `VENDOR.md`: `htmx.min.js` (unpkg htmx.org, ядро без расширений), `alpine.min.js` (unpkg alpinejs `dist/cdn.min.js`, IIFE-сборка), `pico.classless.min.css` (unpkg @picocss/pico). ## 7. Тестирование `httptest`-фейковый control-api + `httptest`-дашборд поверх него, проверка рендера через `strings.Contains` (по образцу `internal/httpapi/handlers_config_test.go`). Кейсы: overview live-агрегация и `resultBreakdown`; submit (happy + пустой список); recheck (done→requeued, checking→skipped, явно показано); cancel (happy + 409); CRUD-раунд-трип + конфликты (409/400) по всем 4 сущностям; control-api недоступен → баннер + 502. **Обязательный ручной шаг:** браузерный смоук-тест против `scripts/run-local-e2e.sh` (или отдельного mock control-api) — все страницы/формы/действия, live-обновление во время реального прогона, проверка через DevTools Network отсутствия внешних (CDN) запросов. ## 8. Документация Новый `docs/DASHBOARD.md`; `README.md` («три компонента» → «четыре»); `docs/SETUP.md` (компонент + раздел развёртывания + сетевые доступы); `docs/API.md` (отсылка на дашборд в предупреждении об открытости API). ## Критичные файлы `internal/dashboard/{client.go,dto.go,routes.go,server.go, handlers_overview.go,render.go,embed.go}`, `internal/config/config.go` (`AdminDashboard`), `cmd/admin-dashboard/main.go`. ## Проверка 1. `go build ./... && go test ./...` 2. `scripts/run-local-e2e.sh` + `admin-dashboard` отдельно против того же control-api 3. Ручной браузерный смоук-тест (обязателен, не пропускается)