docs: детализация дизайна Фазы 3 (расписание, уведомления, метрики)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 12:58:59 +07:00
parent 0c21694ec4
commit 6125af4bab
@@ -385,3 +385,102 @@ internal/api/middleware.go — RequireAuth (session→userID в контек
### Разбивка ### Разбивка
Один план `phase2-auth`. Один план `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`.