Phase 7: monitoring UI — send log, queue, mail.log tail
Three HTMX-polled monitoring screens (spec 7.2.11-13): send log with server-side domain/application filters and pagination, Postfix queue (postqueue -p), and a mail.log tail. Fragment endpoints return HTML snippets, not JSON (spec 7.1); all output is auto-escaped via html/template (spec 7.6.7). Adds store.QuerySendLog/CountSendLog/ListApplicationLogins, postfix.Queue(), and logtail.TailLines (a point-in-time reverse read, independent of the background follow loop). Verified on the dev server: gofmt/vet/test green, docker build green, container e2e (filters, 60-row pagination, <script> escaping, real postqueue/mail.log output, existing Reload button unaffected). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -46,13 +46,23 @@
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
- **Текущая фаза:** 6 ✅ **закрыта** → следующая **Фаза 7** (UI мониторинга: экран журнала отправки + очередь + хвост лога) на **Sonnet** (шаблоны/CRUD, рутинно). Бэкенд журнала (milter + tailer + retention) готов и проверен на сервере.
|
||||
- **Текущая фаза:** 7 ✅ **закрыта** → следующая **Фаза 8** (дифференцированные лимиты, rate limit уровень 2) на **Opus** (логика лимитов в milter — риск-критично).
|
||||
- **Ключевая находка Фазы 6 (исправлена):** go-milter хранит имена макросов **как их шлёт Postfix** — многосимвольные имена приходят в фигурных скобках (`{auth_authen}`, `{client_addr}`), односимвольные — голыми (`i`). Спайк Фазы 0 без SASL этого не увидел (`auth_authen` был пуст «и так»). Первый прогон в контейнере дал пустой `app_login`; фикс — brace-толерантный `macro(m,name)` (пробует голый ключ, затем `{name}`). Зафиксировано в памяти [[milter-implementation-facts]].
|
||||
- **Прежняя фаза:** 5 ✅ закрыта (код `b2692e4`, доки `ec4d4b9`/`2dbd8d0`).
|
||||
- **Финальное подтверждение доставки** (2026-07-13): реальное письмо `dtester@test.example.org → selfpost@mixeme.ru` доставлено и принято `mx.example.net`, заголовок `Authentication-Results: dkim=pass (d=test.example.org s=selfpost) ... spf=pass ... dmarc=none` (прочитано по IMAP). Попало в Junk из-за репутации нового IP/домена (`IP_REPUTATION_SPAM`, Bayes, `MX_INVALID` — у test.example.org только A без MX) — это прогрев IP/DNS уровня деплоя (ТЗ 10), не дефект релея; аутентификация (зона ответственности SelfPost) идеальна.
|
||||
- **Тупик, который обошли (важно для будущих тестов доставки):** нельзя тестировать доставку, отправляя с домена, который хостит сам приёмник. `mx.example.net` хостит `mixeme.ru`, поэтому письма `mixeme.ru → mixeme.ru` он жёстко отбивал `554 does not meet our delivery requirements` (own-domain anti-spoofing) при валидном DKIM+SPF. Решение: отдельный домен-отправитель `test.example.org` (не на `mc`), которому приёмник доверяет как обычной входящей почте. Первый контакт был `451 Greylisted` (норма) → принят после авто-ретраев Postfix.
|
||||
- **Артефакты теста на сервере:** контейнер `p5` (домены mixeme.ru id1 / test.example.org id2), скрипт/лог `/tmp/p5retry.sh`+`/root/p5retry.log`, IMAP-читалка `/tmp/imapread5.py`. DNS `test.example.org` (A/SPF/DKIM) можно снять после Фазы 6-тестов; в ящике `selfpost@mixeme.ru` остались bounce-письма от ранних mixeme.ru→mixeme.ru попыток (шум, можно удалить).
|
||||
|
||||
### Сделано в Фазе 7
|
||||
- **Три экрана мониторинга** (`internal/web/handlers_monitor.go` + шаблоны `sendlog.html`/`queue.html`/`logtail.html` + фрагменты `sendlog_rows.html`/`queue_body.html`/`logtail_body.html`):
|
||||
- **Журнал отправки** (`/sendlog`): таблица время/домен/приложение/From/To/Subject/статус, серверные фильтры по домену и логину приложения (`WHERE` через `store.SendLogFilter`, параметризовано), пагинация (50/страница, `LIMIT/OFFSET`, счётчик страниц через `CountSendLog`), HTMX-polling каждые 5с (`hx-trigger="every 5s"` на самообновляющемся `<div>`, `hx-swap="outerHTML"` — ответ фрагмента несёт те же hx-атрибуты, поэтому поллинг не обрывается). Вывод экранируется автоматически `html/template` (subject с `<script>` проверен — рендерится как `<script>`).
|
||||
- **Очередь Postfix** (`/queue`): `postqueue -p` без изменений (`internal/postfix/queue.go`, фикс-аргумент, без shell), HTMX-polling.
|
||||
- **Хвост mail.log** (`/logtail`): `logtail.TailLines` (`internal/logtail/logtail.go`) — точечное чтение последних 200 строк обратными чанками (не весь файл на каждый polling-тик), независимо от фонового `follow()`-цикла Фазы 6, HTMX-polling.
|
||||
- **Fragment-эндпоинты** (`GET /sendlog/rows`, `/queue/body`, `/logtail/body`) отдают HTML-куски, не JSON (ТЗ 7.1) — отдельный рендер-путь `renderFragment` в `templates.go` (без layout), тот же `{{define}}`-блок используется и полной страницей при первой загрузке, и фрагментом при обновлении — не расходятся.
|
||||
- **Store**: `QuerySendLog`/`CountSendLog` (`internal/store/sendlog.go`, общий `sendLogWhere` для обоих), `ListApplicationLogins` (`internal/store/applications.go`) — источник для выпадающего списка приложений в фильтре.
|
||||
- **Навигация**: ссылки Send log/Queue/Log добавлены в топбар дашборда; на каждой из новых страниц — ссылки на остальные экраны + «Domains».
|
||||
- **Проверено на сервере** (selfpost.example.com, контейнер `p7`): `gofmt`/`vet`/`test` зелёные, `docker build` ок; e2e через curl — все три страницы и все три фрагмента 200, фильтр по домену/приложению сужает выборку, несуществующий домен → «No messages logged yet.», пагинация проверена на 60 строках (страница 1/2 = 50/10 строк, ссылки Newer/Older корректны), `postqueue -p` реально выполняется и возвращает «Mail queue is empty», хвост `mail.log` отдаёт реальные строки Postfix, HTML-экранирование подтверждено (`<script>` в Subject → `<script>`), существующая кнопка Reload и остальные экраны не сломаны.
|
||||
|
||||
### Сделано в Фазе 6
|
||||
- **Journal-milter в чистом Go** (`internal/milter/milter.go`, go-milter v0.4.1): сессия на соединение (`NewMilter`), поля собираются по стадиям (SASL-логин `{auth_authen}`+From на `MailFrom`, каждый получатель на `RcptTo`, `Subject` в `Header`, queue-id `{i}` на EOM/`Body`), запись `send_log` со статусом `queued` **по одной на пару (queue-id, получатель)** (ТЗ 7.3.3), домен деривится из From (привязка отправителя это гарантирует). Строго монитор: колбэки возвращают только Continue/Accept, ошибки записи логируются, **никогда не прокидываются** — приём почты не блокируется. `Protocol: OptNoBody`. Per-message reset на `MailFrom` (несколько писем на соединение). Сокет-жизненный цикл (mkdir/удаление stale/`chmod 0660` для группы `selfpost`) в `cmd/panel/journal.go`; `milter.Serve(ctx, ln, store)` слушает до отмены ctx.
|
||||
- **Log-tailer** (`internal/logtail/logtail.go`): поллинг-хвост `mail.log` (интервал 1с) с обработкой **ротации** (смена inode `os.SameFile` — logrotate `create`; усечение `size<pos` — `copytruncate`), старт с конца файла (не переигрывать историю), накопитель частичной строки. Regex парсит строки доставки (`QID: to=<addr>, … status=<sent|deferred|bounced|expired>`), `expired→bounced`, обновляет `send_log` по (queue-id, получатель) регистронезависимо (`COLLATE NOCASE` — Postfix может менять регистр). qmgr/smtpd-строки игнорируются.
|
||||
@@ -141,3 +151,4 @@
|
||||
- **Фаза 4** (2026-07-12, Opus) — приложения + SASL + привязка к домену: учётки в `sasldb2` через `saslpasswd2` (пароль по stdin, логин whitelisted argv, без shell — 7.6.3), генерируемый пароль показывается один раз (7.6.1), режим адресов wildcard/list с серверной проверкой принадлежности адреса домену (7.6.2), генерация `smtpd_sender_login_maps` (many-to-one слияние, injection-safe — 7.6.4), CRUD приложений + перевыпуск пароля + каскад при удалении домена (очистка `sasldb2` + пересборка карты). **Исправлен reload Postfix:** `signal HUP` не доходит до форкнутого master → одноразовая supervisord-программа `postfix-reload` (настоящий `postfix reload` от root без привилегий панели). `postfix` в группе `selfpost`, `/data/sasl`+`/data/postfix` под setgid. Новые пакеты `internal/app`, `internal/postfix`. Юнит-тесты + контейнерный e2e (весь жизненный цикл, каскад, персистентность, реальный reload по `mail.log`) зелёные.
|
||||
- **Фаза 5** (2026-07-12…13, Opus) — полный исходящий релей Postfix: `smtps` 465 (wrapper TLS) как основной + опциональный `submission` 587 (STARTTLS), SASL (`cyrus`/`sasldb2`, реалм через пустой `smtpd_sasl_local_domain` + `myhostname`), привязка отправителя (`smtpd_sender_login_maps`+`reject_sender_login_mismatch`), без open relay (только по кредам, нет `permit_mynetworks`), исходящая доставка (MX-lookup, TLS may), rate-limit L1 (`anvil`), milter-цепочка с per-milter действиями (OpenDKIM tempfail / journal accept). Конфиг генерируется из env в `postfix-config.sh` (вызов из entrypoint). Два инфра-фикса на сервере: `postconf -F '*/*/chroot=n'` (chroot ломал DNS доставки) и права milter-сокетов (группа `selfpost`+setgid, `chmod 0660` на journal-сокет). **Реальная доставка подтверждена:** `test.example.org → selfpost@mixeme.ru`, `dkim=pass`+`spf=pass` в `Authentication-Results` (по IMAP). Коммиты `b2692e4` (релей), `ec4d4b9`/этот (доки).
|
||||
- **Фаза 6** (2026-07-13, Opus) — journal-milter + обновление статусов Send Log (наивысший риск ТЗ 7.3): milter на go-milter v0.4.1 (запись `send_log` на пару queue-id/получатель на EOM, строго fail-open — колбэки только Continue/Accept), log-tailer с ротацией `mail.log` (парс `sent/deferred/bounced/expired` → апдейт по queue-id+получатель), retention (`SEND_LOG_RETENTION_DAYS`=90, чистка при старте+каждые 6ч), bounded milter-таймауты (15/15/30с) для fail-open при зависании. Store открывается один раз и шарится ролями. **Найден и исправлен** пустой `app_login`: имена макросов приходят в фигурных скобках (`{auth_authen}`) — brace-толерантный `macro()`. Юниты + контейнерный e2e зелёные; **fail-open проверен дважды** (недоступность и зависание), retention проверен. Новая зависимость go-milter (BSD-2).
|
||||
- **Фаза 7** (2026-07-13, Sonnet) — UI мониторинга: три экрана (журнал отправки с серверными фильтрами домен/приложение + пагинацией, очередь Postfix `postqueue -p`, хвост `mail.log`), все с HTMX-polling каждые 5с; fragment-эндпоинты отдают HTML (ТЗ 7.1), вывод экранирован `html/template` (ТЗ 7.6.7, проверено на `<script>` в теме письма). Новое: `store.QuerySendLog/CountSendLog/ListApplicationLogins`, `postfix.Queue()`, `logtail.TailLines` (точечное обратное чтение хвоста, независимо от фонового `follow()`). Юниты/vet/gofmt зелёные; контейнерный e2e (фильтры, пагинация на 60 строках, экранирование, `postqueue -p`, реальные строки `mail.log`, существующий Reload не сломан) — зелёный.
|
||||
|
||||
Reference in New Issue
Block a user