Files
selfpost/docs/documentation-plan.md
T
mix a96b902df0 docs: documentation plan with a code cross-check pass
The documentation is part of the deliverable (spec 11.5/11.7/11.9), so it has
to describe what the code does, not what was intended. Adds
docs/documentation-plan.md: the package inventory against the spec, the
per-claim sources of truth in the tree, the results of a first cross-check
pass (11 findings, most notably the missing "operations" section required by
spec 11.7, the absent env-var reference, .env.example's dangling link to a
README "Rate limiting" section, and the unwritten "tar while stopped" backup
path from spec 9), and tasks D1-D7 gating the next release tag.

progress.md points at it so it survives a context reset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:18:32 +03:00

22 KiB
Raw Blame History

План документации SelfPost

Зачем этот файл. Документация — часть поставки (ТЗ раздел 11, пп. 5, 7, 9), а не сопроводительный текст. Перед тегом релиза она обязана описывать то, что делает код, а не то, что задумывалось: расхождение здесь — такой же дефект, как несоответствие ТЗ, только обнаруживает его пользователь на своём проде. План описывает, из чего состоит пакет, как он сверяется с кодом, что уже разошлось (первый проход выполнен, результаты ниже) и что с этим делать.

Место в общем плане: документационный проход идёт вместе с D.5 (implementation-plan.md) — до тега следующего релиза, после B.1–B.3 и C.4, потому что именно они изменили поведение, которое README описывает (сессии, ротация лога, обязательность SELFPOST_HOSTNAME).

Модель: документация → Sonnet (правило progress.md). Исключение — D6 (HEALTHCHECK/эндпоинт мониторинга) и формулировки про TRUSTED_PROXY_CIDR: инфра/безопасность → Opus.


1. Состав пакета

Поставляемое пользователю (обязательно по ТЗ)

Артефакт Требование Состояние
README.md ТЗ 11 п. 7: установка, требования к площадке, per-domain DNS, прогрев IP, эксплуатация, бэкап/восстановление и экспорт/импорт домена (оба файла — секреты), репозиторий (Codeberg основной / GitHub зеркало), откуда образ (ghcr.io), фиксированный тег, минимальные требования к машине, лицензия в 2–3 предложениях Есть всё, кроме раздела «эксплуатация»; см. находки 1–4, 10
LICENSE ТЗ 11 п. 9: полный текст AGPL-3.0 Полный текст на месте, правок не требует
deploy/docker-compose.yml + фрагменты прокси ТЗ 11 п. 5, ТЗ 10 п. 3: Apache основной, nginx/Caddy/Traefik альтернативами; для каждого — что монтируется и в какие пути Все четыре есть (apache, nginx, caddy, traefik), в README сведены таблицей; см. находки 7, 8
deploy/.env.example Пользовательские переменные с пояснениями Есть 6 переменных; полного справочника нет — находка 3
CHANGELOG.md Keep a Changelog, запись на каждом осмысленном шаге Ведётся; [Unreleased] наполнен B.1B.3, C.4

Рабочие документы проекта (не поставка, но обязаны быть верны)

specification.md — ТЗ v1.0, источник истины по требованиям. implementation-plan.md — открытые вопросы и линия 2.x. progress.md — живой трекер, читается первым после /clear. security.md — принятые риски. Этот файл — план по документации.

Границы: отдельные CONTRIBUTING.md, docs/architecture.md, man-страницы и сайт документации ТЗ не требует и в объём v1.x не входят. Dev-петля (сборка и тесты на dev-сервере) остаётся в progress.md и в память проекта — выносить её в поставляемую документацию не нужно.


2. Метод сверки с кодом

Правило: у каждого утверждения в документации есть ровно один источник истины в дереве, и сверка идёт от кода к тексту (что код делает → сказано ли об этом), а не наоборот — иначе не видно того, что забыли описать.

Класс утверждений Источник истины
Переменные окружения, значения по умолчанию loadConfig/envDefault/envIntcmd/panel/main.go:80159; ${VAR:-default} в build/ (postfix-config.sh, postfix-wrapper.sh, postfix-cert-reload.sh, logrotate-loop.sh, entrypoint.sh)
Поведение почтового тракта (порты, TLS, SASL, анти-relay, лимиты, milter-цепочка) build/postfix-config.sh
Экраны и действия панели (что вообще можно делать в эксплуатации) таблица маршрутов internal/web/web.go:133190
Бэкап/восстановление, экспорт/импорт домена, проверка версии internal/backup/backup.go, cmd/selfpost-backup/main.go
Сессии, вход, смена пароля internal/store/sessions.go, internal/web/session.go, internal/web/handlers_account.go
Ротация лога, периодический reload, интервалы build/logrotate-mail.conf, build/logrotate-loop.sh, build/postfix-cert-reload.sh
Деплой: тег образа, порты, монтирования, capabilities deploy/docker-compose.yml, build/Dockerfile
Требования, а не реализация (что вообще должно быть описано) specification.md разделы 9, 10, 11

Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты, postconf-настройки), потом искать каждый пункт в README — так находится и неверное, и отсутствующее.


3. Результаты первого прохода (выполнен)

Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI, сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4.

Высокий (пробел против ТЗ или вводит в заблуждение)

  1. Нет раздела «эксплуатация» — прямое требование ТЗ 11 п. 7 (specification.md:427). В README нет ни слова про /status (проверки PTR/hostname), /sendlog (журнал отправки, фильтры), /queue (очередь Postfix), /logtail (хвост mail.log), /reload, /account, /backup — при том что всё это реализовано (internal/web/web.go:152–188). Не описана и процедура апгрейда (бамп тега → docker compose up -d), хотя раздел «Fixed image tag» на неё намекает.
  2. Битая ссылка + недокументированные лимиты. deploy/.env.example:13 отсылает к разделу README «Rate limiting», которого в README нет. Сам двухуровневый лимит не описан нигде для пользователя: L1 — smtpd_client_message_rate_limit + anvil_rate_time_unit (build/postfix-config.sh:110), L2 — per-domain/per-app из панели (internal/web/web.go:165, :169), с записью rejected в журнал.
  3. Нет справочника переменных окружения. В .env.example шесть штук; код читает заметно больше. Панель: SELFPOST_DATA_DIR, SELFPOST_DB_PATH, SELFPOST_SETUP_TOKEN_FILE, PANEL_HTTP_ADDR, JOURNAL_MILTER_SOCKET, MAIL_LOG, PANEL_COOKIE_SECURE, TLS_CERT_FILE, OPENDKIM_SOCKET, OPENDKIM_DIR, DKIM_SELECTOR_DEFAULT, SASL_DB_PATH, SASL_REALM, POSTFIX_DIR (cmd/panel/main.go:80127). Обвязка: TLS_KEY_FILE, POSTFIX_SENDER_LOGIN_MAPS, MILTER_CONNECT_TIMEOUT/COMMAND/CONTENT (15s/15s/30s, build/postfix-config.sh:133), MILTER_WAIT_TIMEOUT, TLS_RELOAD_INTERVAL_SECONDS (86400, build/postfix-cert-reload.sh:14), LOGROTATE_INTERVAL_SECONDS (21600, build/logrotate-loop.sh:17). Отдельно: TRUSTED_PROXY_CIDR — переменная с последствиями для безопасности (доверие X-Forwarded-For при rate-limit логина), описана только в .env.example, в README её нет вообще. Решение, которое надо принять в задаче D2: какие переменные публичные (таблица в README), а какие внутренние (достаточно комментария в коде) — документировать все 20+ вредно, они станут «поддерживаемым интерфейсом».
  4. Оговорка ТЗ 9 про прямой tar не отражена. specification.md:375 требует описать оба пути: «бэкап на лету — панель/CLI» и «прямой tar директории безопасен, если контейнер остановлен». В README (Backup, restore…) есть только первый, поэтому у читателя нет ответа на очевидный вопрос «а можно просто заархивировать ./data?».

Средний (стало неверным после последних фаз)

  1. Статус-баннер устарел. README.md:1215: «under active development» и ссылка на implementation-plan.md как на «phased build plan» — фазы 0→14 закрыты, план теперь про открытые вопросы и линию 2.x. Перед тегом релиза баннер переформулировать (или снять), ссылки уточнить.
  2. implementation-plan.md разошёлся с кодом. Пункт B.1 говорит: «смена пароля завершает все сессии, включая ту, из которой её делают → редирект на /login». Код делает иначе — гасит все, кроме текущей (internal/store/sessions.go:95 DeleteOtherSessions, internal/web/session.go:131), и панель так и пишет («Any other signed-in sessions were signed out», internal/web/handlers_account.go:49). README здесь верен. Расхождение в плане — зафиксировать как осознанное изменение при реализации, а не молча переписать.
  3. Комментарий в compose вводит в заблуждение. deploy/docker-compose.yml:14 велит «fill in .env (hostname, at least one strong TLS_CERT/KEY path)», но TLS_CERT_FILE/TLS_KEY_FILE захардкожены в самом файле (:3031) и в .env.example отсутствуют — настраивается на деле bind mount ./certs, а не переменные.
  4. Порт 587 публикуется всегда (deploy/docker-compose.yml:61), хотя listener появляется только при SUBMISSION_ENABLE=true (build/postfix-config.sh:151). Безвредно (слушать некому), но выглядит как лишний открытый порт — одна строка пояснения в README/compose снимает вопрос.

Низкий / требует решения, а не только текста

  1. /healthz есть, HEALTHCHECK нет. Эндпоинт реализован (internal/web/web.go:133), в build/Dockerfile объявления HEALTHCHECK нет (пробел отмечен и в плане, п. B.3). Решить в D6: либо задокументировать /healthz как точку внешнего мониторинга, либо добавить HEALTHCHECK в образ (это уже код + строка в CHANGELOG). Документировать «здоровье контейнера» до принятия решения нельзя — получится обещание, которого образ не даёт.
  2. Из B.1B.3/C.4 в README попала только обязательность SELFPOST_HOSTNAME. Не описаны: сессия переживает рестарт и живёт по скользящему сроку бездействия (PANEL_SESSION_IDLE_DAYS, «вкладка с автообновлением вход не продлевает» — контринтуитивно и заслуживает строки), ротация mail.log (14 файлов, проверка каждые 6 ч, суточный postfix reload) — README упоминает только «kept 14 days in-image» в требованиях к машине.
  3. Мелочи, без правки кода: Quick start тянет файлы с raw.githubusercontent.com (зеркало), хотя основной репозиторий — Codeberg; тег образа 1.0.0 в compose (:24) придётся бампить в тот же коммит, что и релиз; пустой каталог docs/logo.

Проверено и расхождений не найдено (важно не переделывать): требования к площадке; DNS-раздел (совпадает с тем, что реально проверяет /status и DNS-карточка домена); прогрев IP; смысл фиксированного тега; проверка версии при восстановлении — README описывает её точно так, как ведёт себя CheckRestore (internal/backup/backup.go); форма вызова docker exec … selfpost-backup > … (cmd/selfpost-backup/main.go:7); таблица reverse-proxy и требование пропускать Host; секретность архива и экспорта домена; лицензия; ретенция журнала отправки (90 дней) и то, что она — основной драйвер роста /data.


4. Задачи

Порядок — как перечислены; D1–D3 самые крупные. Каждая заканчивается записью в [Unreleased] CHANGELOG.md и коммитом.

D1. README: раздел «Operations» + «Rate limiting». Закрывает находки 1, 2 и часть 10. Что описать: экраны панели и зачем они нужны (/status и что именно он проверяет; журнал отправки со статусами queued/sent/rejected; очередь; хвост mail.log; /reload; смена учётных данных); двухуровневый лимит с именами переменных L1 и указанием, что L2 задаётся в панели per-domain/app; процедура апгрейда версии; поведение сессии (скользящий срок, опросы не продлевают, смена пароля гасит остальные). Тон и объём — как в существующих разделах: практика, а не пересказ ТЗ. Готово, когда: каждый маршрут из internal/web/web.go:152188, кроме HTMX-фрагментов, либо описан, либо сознательно опущен; ссылка «Rate limiting» из .env.example ведёт в существующий якорь.

D2. Справочник переменных окружения. Закрывает находку 3. Сначала решение о границе публичного набора, затем таблица (имя, назначение, дефолт, где задаётся) в README + синхронизация .env.example и комментариев в compose. TRUSTED_PROXY_CIDR описать с прямым предупреждением: неверное значение позволяет подделать ключ rate-limit'а логина. Готово, когда: каждый ключ из loadConfig и из ${VAR:-…} в build/*.sh попал либо в таблицу, либо в явный список «внутренние, не интерфейс»; дефолты в таблице совпадают с кодом дословно.

D3. Бэкап: дописать путь «остановленный контейнер + tar». Закрывает находку 4 — по формулировке specification.md:375, с явным «на живом контейнере так делать не надо, потому что SQLite в WAL». Заодно упомянуть, что manifest.json после успешного восстановления потребляется.

D4. Точечные правки. Находки 5, 7, 8, 11: баннер статуса и ссылки; комментарий в шапке compose про .env; строка про 587; бамп тега образа (делается в релизном коммите, не раньше).

D5. Синхронизация внутренних документов. Находка 6 — поправить B.1 в implementation-plan.md (с пометкой, что решение изменилось при реализации и почему), заодно перечитать progress.md на предмет утверждений, которые код уже опроверг. Не переписывать историю в CHANGELOG.

D6. Решение по HEALTHCHECK//healthz (Opus). Находка 9: либо строка в Dockerfile + описание, либо только описание эндпоинта. Влияет на код образа — поэтому отдельной задачей и не на Sonnet.

D7. Регресс-защита (см. раздел 5).


5. Чтобы не разошлось снова

  1. Правило шага: изменение, добавляющее/переименовывающее env-переменную, маршрут панели или наблюдаемое поведение почтового тракта, закрывается только вместе с правкой README/.env.example — в том же коммите, наравне с записью в CHANGELOG (протокол закрытия шага в progress.md).
  2. Дешёвая машинная проверка (D7): тест или скрипт, сверяющий множество ключей loadConfig со списком, объявленным в документации, и падающий на новом недокументированном ключе. Ловит самый частый класс расхождений (находка 3) без ручного прохода. Границу «внутренних» ключей задать явным списком-исключением в самом тесте.
  3. Перед каждым тегом — короткий проход по разделу 2 этого файла (семь источников истины), а не полная ревизия текста.

6. Гейт релиза

Документационный проход — часть того же гейта, что e2e (C.4) и ревизия безопасности (D.5): D1–D6 закрыты до тега. D7 — желателен, но тег не блокирует. Находки, обнаруженные позже, дописываются сюда, а не исправляются молча: этот файл — журнал состояния документации, а не одноразовый список дел.