diff --git a/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md b/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md index b19746c..f52f31f 100644 --- a/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md +++ b/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md @@ -385,3 +385,102 @@ internal/api/middleware.go — RequireAuth (session→userID в контек ### Разбивка Один план `phase2-auth`. + +## Фаза 3 — детализация (расписание · уведомления · метрики) + +Периодические проверки доменов по расписанию, уведомления о смене статуса, Prometheus-метрики. +Планировщик **только проверяет и уведомляет** — apply остаётся ручным (prune-guard из 1A/1B). + +### Решения + +- **Планировщик — встроенный in-process**: goroutine-тикер в `cmd/server`, интервал из БД, `last_run_at` + персистентен (переживает рестарт). Без внешних зависимостей. +- **Гранулярность — на проект**: единый интервал проверки всех доменов проекта. +- **Каналы уведомлений — Telegram + Webhook** (чистый `net/http`, без внешних lib). Telegram — + Bot API `sendMessage`; Webhook — HTTP POST JSON. +- **Уведомление при СМЕНЕ статуса домена** (in_sync ↔ drift ↔ error), не спам каждый тик — + `domains.last_check_status`. +- **Метрики — Prometheus** (`client_golang`, custom registry), `/metrics` **публичный** (scrape), + без секретов. +- **bot_token Telegram шифруется** (тот же `crypto.Cipher`, что provider-секреты); в ответах не отдаётся. + +### БД (миграция 0004) + +``` +CREATE TABLE schedules ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL UNIQUE REFERENCES projects(id) ON DELETE CASCADE, + interval_seconds int NOT NULL DEFAULT 3600, + enabled boolean NOT NULL DEFAULT false, + last_run_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE TABLE notification_channels ( + id uuid PRIMARY KEY, + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + type text NOT NULL, -- telegram | webhook + config jsonb NOT NULL, -- telegram: {chat_id}; webhook: {url} + secret_enc text NOT NULL DEFAULT '', -- telegram: bot_token (шифр); webhook: пусто/подпись + enabled boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE domains ADD COLUMN last_check_status text NOT NULL DEFAULT 'unknown'; -- unknown|in_sync|drift|error +``` + +### Backend + +``` +internal/notify + ├─ notify.go — интерфейс Notifier{Send(ctx, Event) error}, тип Event{Project,Domain,Status,Summary,At} + ├─ telegram.go — TelegramNotifier (Bot API sendMessage, net/http) + ├─ webhook.go — WebhookNotifier (HTTP POST JSON) + └─ dispatch.go — Dispatcher: по каналам проекта шлёт Event через нужный Notifier (расшифровка secret) +internal/scheduler + └─ scheduler.go — Scheduler{Run(ctx)}: тикер → due-проекты (enabled && now-last_run>=interval) + → для каждого домена service.Check → сохранить check_run + обновить last_check_status + → при смене статуса → Dispatcher.Send; обновить last_run_at +internal/metrics + └─ metrics.go — Metrics{registry, counters/histogram/gauge}; Handler() для /metrics +``` + +- **Метрики** (custom `prometheus.Registry`, `promauto`): `dns_ar_checks_total{status}` (counter), + `dns_ar_check_duration_seconds` (histogram), `dns_ar_drift_domains` (gauge — текущее число в дрейфе), + `dns_ar_notifications_total{channel,status}` (counter). Инструментируются scheduler/service/notify. + `/metrics` — `promhttp.HandlerFor(reg, ...)`, публичный. +- **Планировщик**: последовательная обработка проектов (без гонок по проекту), graceful shutdown по + `context`. Ошибка проверки домена → статус `error` + метрика + уведомление. + +### REST API (`/api/v1/projects/{pid}`, под RequireAuth+RequireProjectAccess) + +| Метод/путь | Назначение | +|---|---| +| `GET/PUT /schedule` | получить/задать расписание проекта (interval_seconds, enabled) | +| `POST/GET/DELETE /channels` | CRUD каналов уведомлений (secret на вход, не на выход) | +| `POST /channels/{cid}/test` | тест-отправка уведомления в канал | +| `GET /domains/{did}/history` | история проверок (check_runs) домена | + +### Frontend + +- **SchedulePage** (или блок в проекте): интервал + вкл/выкл. +- **ChannelsPage**: CRUD каналов (Telegram: chat_id + bot_token; Webhook: url), тест-отправка. Секрет только на вход. +- **История проверок** домена (список check_runs с результатом/временем). +- **drift-badge** в списке доменов: `last_check_status` (in_sync=emerald, drift=amber, error=rose, unknown=muted). + +### Инварианты + +- Планировщик не применяет изменения (только check + notify). +- `bot_token`/секреты каналов не в ответах/логах; `/metrics` без секретов и PII (только агрегаты по status/channel). +- Уведомления идемпотентны по смене статуса (нет спама при неизменном дрейфе). +- Всё scoped по проекту (мультитенантность/IDOR из Фазы 2 сохраняется). + +### Тестирование Фазы 3 + +- `notify` — Telegram/Webhook против `httptest` (корректный запрос, обработка ошибки); Dispatcher (выбор канала, расшифровка). +- `scheduler` — мок store/service/notifier, управляемый «тик»: due-выбор, смена статуса → уведомление, идемпотентность (нет уведомления при неизменном статусе), ошибка → error-статус. +- `metrics` — `testutil` (счётчики/гистограмма инкрементируются), `/metrics` отдаёт. +- API — httptest + store (CRUD schedule/channels, secret не в ответе, история). +- Frontend — Vitest (клиент/хуки, drift-badge, формы каналов, тест-отправка). + +### Разбивка + +Один план `phase3-scheduler-notify-metrics`.