Phase 13. Two new packages and one new screen. internal/health owns the shared status vocabulary (ok/warn/error/unknown) and the local checks: supervisord's process table, TLS certificate expiry and the two milter sockets. Each check reports a problem as a status rather than an error, so one broken component costs a line and not the page. internal/dnscheck does the read-only lookups: forward-confirmed reverse DNS for SELFPOST_HOSTNAME, and per-domain DKIM (compared against the key this server actually signs with), SPF and DMARC. Every check is bounded by a timeout and cached, and the resolver sits behind an interface so the tests drive every branch without touching the network. The SPF check is deliberately shallow: it looks for a mechanism literally covering the server's address and does not follow include:/redirect=, so a record that authorises us through an include is reported as "cannot tell" rather than as a failure. /status renders both, with the local checks in an HTMX-polled fragment and the DNS lookups behind a Re-check button, and becomes the panel's landing page: / now redirects there and the domain list lives at /domains. The Reload button moves onto /status, where it reads as what it is — a drift-recovery for the daemons — with text explaining what it regenerates. A template test fails on any remaining href="/" so a stale link cannot silently land on the wrong screen. Also fixes a defect this made visible: the panel could never read the mail queue in the documented deployment. postqueue relies on its setgid-postdrop bit, which the compose file's no-new-privileges disables, so the Queue screen always said "Could not read the mail queue" — including in the released 1.0.0 image. The panel user is now a real member of postdrop, which needs no setgid transition. Verified in a container on the dev server against real DNS: PTR matching (selfpost.example.com) and not matching (example.com), DKIM absent and mismatched, SPF absent and via include:, DMARC p=quarantine/p=reject/absent, and a resolver timeout degrading to "unknown" without hanging the page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.2 KiB
Прогресс реализации SelfPost
Живой трекер состояния. Переживает /clear — читается первым при возобновлении работы.
План (открытые вопросы + опциональные фазы): implementation-plan.md.
ТЗ: specification.md. История релизов: CHANGELOG.md.
История сделанного по фазам (0→11, все закрыты) — в git log и в CHANGELOG, здесь не дублируется.
Как возобновить после сброса контекста
- Прочитать этот файл (текущее состояние, что дальше).
- Открыть
implementation-plan.md— там нерешённые вопросы и опциональная линия 2.x.x (Фаза O1+). - При необходимости — детали в
specification.md. - Продолжить с пункта «Следующий шаг».
Модель по типу работы
Правило: безопасность / инфра / риск-критичное → Opus; UI / документация / бойлерплейт → Sonnet; тривиальная механика → Haiku.
Коммиты
Коммит на каждом осмысленном шаге (не каждое сохранение файла, но и не только конец фазы): рабочий под-функционал, зелёная сборка, конец фазы. Минимум — один коммит на закрытую фазу + промежуточные на связные под-шаги. Ветка main (если пользователь не попросит отдельную). Push/PR — только по явной команде. Сообщение коммита завершается трейлером Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>.
На каждом таком шаге — запись в CHANGELOG.md под [Unreleased] (формат Keep a Changelog). При явном решении зарезать версию — секция [Unreleased] переименовывается в [X.Y.Z] - дата, заводится новая пустая [Unreleased]. Тег/пуш образа — только по явному запросу (см. workflow release.yml).
Протокол закрытия фазы/крупного шага
Перед /clear в конце каждого законченного шага Claude:
- Обновляет этот файл: «Текущее состояние» → что изменилось, что дальше.
- Проверяет применимые критерии «Готово, когда…».
- Дописывает
CHANGELOG.mdпод[Unreleased]. - Делает финальный коммит шага.
Текущее состояние
- Базовый линейный план 0→11 (v1.0) полностью выполнен и принят (аудит безопасности ТЗ 7.6 — полное соответствие, деплой в проде подтверждён). Подробности — в git-истории и
CHANGELOG.md. - Фаза 12 (v1.x, UI/UX) выполнена: общий
nav-partial вlayout.html(нав на каждой аутентифицированной странице + подсветка текущей черезActive),/account(смена логина/пароля админа с проверкой текущего пароля и инвалидацией остальных сессий,store.UpdateAdmin),/backup(полный бэкап и импорт домена — две отдельные карточки, убраны с дашборда), карточка «Sending server settings» на странице домена (сервер/465/587 поSUBMISSION_ENABLE), кнопки Copy у DKIM-записи и нового пароля приложения, скрытие поля «Addresses» в режиме wildcard (static/panel.js). Проверено в контейнере на dev-сервере (setup→login→домен→приложение→смена пароля→импорт),gofmt/vet/test/docker buildзелёные. - Фаза 13 (v1.x, статус + DNS) выполнена: новый пакет
internal/health(supervisorctl-статус процессов, срок действия TLS-сертификата, milter-сокеты + общий словарь статусов ok/warn/error/unknown) иinternal/dnscheck(FCrDNS дляSELFPOST_HOSTNAME, DKIM против реального ключа, поверхностная SPF-эвристика, DMARC; кеш + таймаут, резолвер за интерфейсом — юнит-тесты без сети). Страница/status(карточки + HTMX-фрагмент/status/fragmentна 5с) стала стартовой:GET /→ 303 на/status, список доменов переехал наGET /domains, кнопка Reload перенесена на/statusс пояснением. На странице домена — карточка «DNS status» с кнопкой Re-check (POST /domains/{id}/dns-recheck). Проверено в контейнере на dev-сервере против реального DNS: PTR совпадает (selfpost.example.com) и не совпадает (example.com), DKIM отсутствует/не совпадает, SPF отсутствует/черезinclude:(«не могу сказать»), DMARCp=quarantine/p=reject/нет; таймаут DNS деградирует в «unknown», страница не виснет. - Попутно исправлен давний дефект: панель никогда не могла прочитать очередь (
postqueue -p) в штатном деплое —postqueueполагается на setgid-битpostdrop, аno-new-privilegesиз поставляемого compose его отключает, поэтому экран Queue всегда показывал «Could not read the mail queue» (в т.ч. в released 1.0.0). Пользовательpanelтеперь состоит в группеpostdrop(build/Dockerfile). - Дальше — то, что перечислено в
implementation-plan.md: открытые вопросы (раздел A-D — hardening сверх обязательного 7.6, надёжность, CI/тесты), Фаза 14 — security-заголовки + документация про/data/setup-token; опциональная Фаза O1+ (входящий релей, линия 2.x.x, требует согласования). - Прод:
selfpost.example.com, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass).
Рабочая петля (dev loop) — ВАЖНО
Локально (Windows, <repo>) нет Go и Docker — только редактирование и git. Вся сборка/тесты идут на dev-сервере selfpost.example.com (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → залить дерево на сервер → go build/go vet/docker build/тесты там. rsync в локальном git-bash нет, поэтому дерево едет tar'ом по ssh: tar -czf - --exclude=.git . | ssh root@selfpost.example.com 'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'; Go на сервере — в /usr/local/go/bin (не в PATH по умолчанию); образ — docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .. Источник истины и git-история — локальный репозиторий; сервер — только исполнитель сборки/тестов. Подключение: ssh root@selfpost.example.com (по ключу).