docs: детализация дизайна Фазы 3 (расписание, уведомления, метрики)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user