# План документации SelfPost **Зачем этот файл.** Документация — часть поставки (deliverables v1.0, пп. 5, 7, 9 в [specification.md](specification.md)), а не сопроводительный текст. Перед тегом релиза она обязана описывать **то, что делает код**, а не то, что задумывалось: расхождение здесь — такой же дефект, как несоответствие требованиям, только обнаруживает его пользователь на своём проде. План описывает, из чего состоит пакет, как он сверяется с кодом, что уже разошлось (первый проход выполнен, результаты ниже) и что с этим делать. **Цель после выполнения плана:** [specification.md](specification.md) (ТЗ v1.0) **выводится из обращения** — не потому что требования исчезли, а потому что v1.0 реализован и каждый блок ТЗ получает постоянный дом: пользовательское — в README, устройство — в `architecture.md`, продуктовые границы — в `product.md`, обязательная безопасность — в `security.md`, процесс разработки — в `development.md`. Живой `specification.md` после этого держит два риска: дублирование с кодом и ложное ощущение «источника истины», который уже не сверяют. Задача **D9** — миграция остатков и снятие файла; до её закрытия ссылки на ТЗ в этом файле — исторические. **Место в общем плане:** документационный проход идёт вместе с **D.5** ([implementation-plan.md](implementation-plan.md)) — до тега следующего релиза, после B.1–B.3 и C.4, потому что именно они изменили поведение, которое README описывает (сессии, ротация лога, обязательность `SELFPOST_HOSTNAME`). **Модель:** документация → Sonnet (правило [progress.md](progress.md)). Исключение — **D6** (HEALTHCHECK/эндпоинт мониторинга) и формулировки про `TRUSTED_PROXY_CIDR`: инфра/безопасность → Opus. --- ## 1. Состав пакета ### Поставляемое пользователю (обязательно по ТЗ) | Артефакт | Требование | Состояние | |---|---|---| | [README.md](../README.md) | ТЗ 11 п. 7: установка, требования к площадке, per-domain DNS, прогрев IP, **эксплуатация**, бэкап/восстановление и экспорт/импорт домена (оба файла — секреты), репозиторий (Codeberg основной / GitHub зеркало), откуда образ (`ghcr.io`), фиксированный тег, минимальные требования к машине, лицензия в 2–3 предложениях | Есть всё, **кроме раздела «эксплуатация»**; см. находки 1–4, 10 | | [LICENSE](../LICENSE) | ТЗ 11 п. 9: полный текст AGPL-3.0 | Полный текст на месте, правок не требует | | [deploy/docker-compose.yml](../deploy/docker-compose.yml) + фрагменты прокси | ТЗ 11 п. 5, ТЗ 10 п. 3: Apache основной, nginx/Caddy/Traefik альтернативами; для каждого — что монтируется и в какие пути | Все четыре есть ([apache](../deploy/apache/), [nginx](../deploy/nginx/), [caddy](../deploy/caddy/), [traefik](../deploy/traefik/)), в README сведены таблицей; см. находки 7, 8 | | [deploy/.env.example](../deploy/.env.example) | Пользовательские переменные с пояснениями | Есть 6 переменных; полного справочника нет — находка 3 | | [CHANGELOG.md](../CHANGELOG.md) | Keep a Changelog, запись на каждом осмысленном шаге | Ведётся; `[Unreleased]` наполнен B.1–B.3, C.4 | ### Рабочие документы проекта (не поставка, но обязаны быть верны) [specification.md](specification.md) — ТЗ v1.0; **на вывод после D9** (см. карту миграции ниже). До закрытия D9 — ещё используется как историческая ссылка в находках раздела 3. [implementation-plan.md](implementation-plan.md) — открытые вопросы для v1.0/v1.x. [roadmap.md](roadmap.md) — линия 2.x.x. [progress.md](progress.md) — живой трекер, читается первым после `/clear`. [security.md](security.md) — обязательные требования безопасности + принятые риски. Этот файл — план по документации. **Новые `product.md`, `architecture.md`, `development.md` — решение принято.** В обязательную поставку пользователю (README) они не входят, но без них понимание проекта держится на памяти сессии. Их содержимое **заменяет** живой `specification.md` для всех будущих проходов — задачи **D8** и **D9**. - `docs/product.md` — из ТЗ §1–3 и §4.1: зачем SelfPost, инфраструктурные предпосылки, out of scope, мультидоменная модель «домен ↔ приложения ↔ режим From». Границы продукта, меняющиеся только явным решением. - `docs/architecture.md` — as-built по коду: `supervisord`, `postfix`, `opendkim`, panel (HTTP + journal-milter + log-tailer); порядок старта; milter-цепочка; Postfix/SASL/TLS; журнал отправки и L1/L2 rate-limit; бэкап/restore; персистентность `/data`. Источник истины — код, не ТЗ. - `docs/development.md` — Go локально, `go test`/`go vet`, `make e2e`, когда нужен полный контейнер на dev-сервере, ручная проверка на `selfpost.mixfed.ru`, протокол коммитов/CHANGELOG, правила для агента (бывшее ТЗ §12). В `progress.md` — текущее состояние, не процедура. ### Карта миграции из `specification.md` | Блок ТЗ | Новый дом | Задача | |---|---|---| | §1–3, §4.1 | `product.md` | D9 | | §4, §5–7 (техника), §9 | `architecture.md` | D8 | | §7.6 | `security.md` («Обязательные требования») | D9 | | §8 | README (таблица env) | D2 | | §9 (путь `tar` для пользователя) | README | D3 | | §10–11 | README + таблица «Поставка» выше | D1–D4 | | §12 | `development.md` | D8 | После D9: `specification.md` → `docs/archive/specification-v1.0.md` (снимок, не правится); в `docs/` живых ссылок на него нет. **Границы (без изменений):** отдельные `CONTRIBUTING.md`, man-страницы и сайт документации в объём v1.x не входят. --- ## 2. Метод сверки с кодом Правило: у каждого утверждения в документации есть **ровно один источник истины в дереве**, и сверка идёт от кода к тексту (что код делает → сказано ли об этом), а не наоборот — иначе не видно того, что забыли описать. | Класс утверждений | Источник истины | |---|---| | Переменные окружения, значения по умолчанию | `loadConfig`/`envDefault`/`envInt` — [cmd/panel/main.go:80](../cmd/panel/main.go:80)–159; `${VAR:-default}` в [build/](../build/) (`postfix-config.sh`, `postfix-wrapper.sh`, `postfix-cert-reload.sh`, `logrotate-loop.sh`, `entrypoint.sh`) | | Поведение почтового тракта (порты, TLS, SASL, анти-relay, лимиты, milter-цепочка) | [build/postfix-config.sh](../build/postfix-config.sh) | | Экраны и действия панели (что вообще можно делать в эксплуатации) | таблица маршрутов [internal/web/web.go:133](../internal/web/web.go:133)–190 | | Бэкап/восстановление, экспорт/импорт домена, проверка версии | [internal/backup/backup.go](../internal/backup/backup.go), [cmd/selfpost-backup/main.go](../cmd/selfpost-backup/main.go) | | Сессии, вход, смена пароля | [internal/store/sessions.go](../internal/store/sessions.go), [internal/web/session.go](../internal/web/session.go), [internal/web/handlers_account.go](../internal/web/handlers_account.go) | | Ротация лога, периодический reload, интервалы | [build/logrotate-mail.conf](../build/logrotate-mail.conf), [build/logrotate-loop.sh](../build/logrotate-loop.sh), [build/postfix-cert-reload.sh](../build/postfix-cert-reload.sh) | | Деплой: тег образа, порты, монтирования, capabilities | [deploy/docker-compose.yml](../deploy/docker-compose.yml), [build/Dockerfile](../build/Dockerfile) | | Обязательное содержание README (чеклист поставки) | таблица «Поставка» в §1 этого файла (бывшее ТЗ §10–11) | | Границы продукта, out of scope | [product.md](product.md) *(после D9)* | | As-built устройство (не пользовательский текст) | [architecture.md](architecture.md) *(после D8)* | | Обязательные требования безопасности | [security.md](security.md) *(после D9)* | Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты, `postconf`-настройки), потом искать каждый пункт в README / `architecture.md` — так находится и неверное, и **отсутствующее**. До закрытия D9 для чеклиста README допустима сверка с `specification.md` §10–11; после D9 — только с §1 этого файла. --- ## 3. Результаты первого прохода (выполнен) Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI, сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4. **Высокий (пробел против ТЗ или вводит в заблуждение)** 1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7 ([specification.md:427](specification.md)). В README нет ни слова про `/status` (проверки PTR/hostname), `/deliveries` (журнал отправки, фильтры), `/mail-queue` (очередь Postfix), `/system-log` (хвост `mail.log`), `/reload`, `/account`, `/backup` — при том что всё это реализовано ([internal/web/web.go:152](../internal/web/web.go:152)–188). Не описана и процедура апгрейда (бамп тега → `docker compose up -d`), хотя раздел «Fixed image tag» на неё намекает. 2. **Битая ссылка + недокументированные лимиты.** [deploy/.env.example:13](../deploy/.env.example:13) отсылает к разделу README «Rate limiting», которого в README нет. Сам двухуровневый лимит не описан нигде для пользователя: L1 — `smtpd_client_message_rate_limit` + `anvil_rate_time_unit` ([build/postfix-config.sh:110](../build/postfix-config.sh:110)), L2 — per-domain/per-app из панели ([internal/web/web.go:165](../internal/web/web.go:165), [:169](../internal/web/web.go: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:80](../cmd/panel/main.go:80)–127). Обвязка: `TLS_KEY_FILE`, `POSTFIX_SENDER_LOGIN_MAPS`, `MILTER_CONNECT_TIMEOUT`/`COMMAND`/`CONTENT` (15s/15s/30s, [build/postfix-config.sh:133](../build/postfix-config.sh:133)), `MILTER_WAIT_TIMEOUT`, `TLS_RELOAD_INTERVAL_SECONDS` (86400, [build/postfix-cert-reload.sh:14](../build/postfix-cert-reload.sh:14)), `LOGROTATE_INTERVAL_SECONDS` (21600, [build/logrotate-loop.sh:17](../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](specification.md) требует описать оба пути: «бэкап на лету — панель/CLI» и «прямой `tar` директории безопасен, если контейнер остановлен». В README ([Backup, restore…](../README.md)) есть только первый, поэтому у читателя нет ответа на очевидный вопрос «а можно просто заархивировать `./data`?». **Средний (стало неверным после последних фаз)** 5. **Статус-баннер устарел.** [README.md:12](../README.md:12)–15: «under active development» и ссылка на `implementation-plan.md` как на «phased build plan» — фазы 0→14 закрыты, план теперь про открытые вопросы и линию 2.x. Перед тегом релиза баннер переформулировать (или снять), ссылки уточнить. 6. **`implementation-plan.md` разошёлся с кодом.** Пункт B.1 говорит: «смена пароля завершает **все** сессии, включая ту, из которой её делают → редирект на `/login`». Код делает иначе — гасит все, **кроме** текущей ([internal/store/sessions.go:95](../internal/store/sessions.go:95) `DeleteOtherSessions`, [internal/web/session.go:131](../internal/web/session.go:131)), и панель так и пишет («Any other signed-in sessions were signed out», [internal/web/handlers_account.go:49](../internal/web/handlers_account.go:49)). README здесь **верен**. Расхождение в плане — зафиксировать как осознанное изменение при реализации, а не молча переписать. 7. **Комментарий в compose вводит в заблуждение.** [deploy/docker-compose.yml:14](../deploy/docker-compose.yml:14) велит «fill in .env (hostname, at least one strong TLS_CERT/KEY path)», но `TLS_CERT_FILE`/`TLS_KEY_FILE` захардкожены в самом файле ([:30](../deploy/docker-compose.yml:30)–31) и в `.env.example` отсутствуют — настраивается на деле **bind mount** `./certs`, а не переменные. 8. **Порт 587 публикуется всегда** ([deploy/docker-compose.yml:61](../deploy/docker-compose.yml:61)), хотя listener появляется только при `SUBMISSION_ENABLE=true` ([build/postfix-config.sh:151](../build/postfix-config.sh:151)). Безвредно (слушать некому), но выглядит как лишний открытый порт — одна строка пояснения в README/compose снимает вопрос. **Низкий / требует решения, а не только текста** 9. **`/healthz` есть, `HEALTHCHECK` нет.** Эндпоинт реализован ([internal/web/web.go:133](../internal/web/web.go:133)), в [build/Dockerfile](../build/Dockerfile) объявления `HEALTHCHECK` нет (пробел отмечен и в плане, п. B.3). Решить в D6: либо задокументировать `/healthz` как точку внешнего мониторинга, либо добавить `HEALTHCHECK` в образ (это уже код + строка в CHANGELOG). Документировать «здоровье контейнера» до принятия решения нельзя — получится обещание, которого образ не даёт. 10. **Из B.1–B.3/C.4 в README попала только обязательность `SELFPOST_HOSTNAME`.** Не описаны: сессия переживает рестарт и живёт по скользящему сроку бездействия (`PANEL_SESSION_IDLE_DAYS`, «вкладка с автообновлением вход не продлевает» — контринтуитивно и заслуживает строки), ротация `mail.log` (14 файлов, проверка каждые 6 ч, суточный `postfix reload`) — README упоминает только «kept 14 days in-image» в требованиях к машине. 11. **Мелочи, без правки кода:** Quick start тянет файлы с `raw.githubusercontent.com` (зеркало), хотя основной репозиторий — Codeberg; тег образа `1.0.0` в compose ([:24](../deploy/docker-compose.yml:24)) придётся бампить в тот же коммит, что и релиз; пустой каталог `docs/logo`. **Проверено и расхождений не найдено** (важно не переделывать): требования к площадке; DNS-раздел (совпадает с тем, что реально проверяет `/status` и DNS-карточка домена); прогрев IP; смысл фиксированного тега; проверка версии при восстановлении — README описывает её точно так, как ведёт себя `CheckRestore` ([internal/backup/backup.go](../internal/backup/backup.go)); форма вызова `docker exec … selfpost-backup > …` ([cmd/selfpost-backup/main.go:7](../cmd/selfpost-backup/main.go:7)); таблица reverse-proxy и требование пропускать `Host`; секретность архива и экспорта домена; лицензия; ретенция журнала отправки (90 дней) и то, что она — основной драйвер роста `/data`. --- ## 4. Задачи Порядок — как перечислены; D1–D3 самые крупные. Каждая заканчивается записью в `[Unreleased]` [CHANGELOG.md](../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:152](../internal/web/web.go:152)–188, кроме 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](specification.md), с явным «на живом контейнере так делать не надо, потому что SQLite в WAL». Заодно упомянуть, что `manifest.json` после успешного восстановления потребляется. **D4. Точечные правки.** Находки 5, 7, 8, 11: баннер статуса и ссылки (убрать ссылку на `specification.md` — после D9 продукт в `product.md`); комментарий в шапке compose про `.env`; строка про 587; бамп тега образа (делается в релизном коммите, не раньше). **D5. Синхронизация внутренних документов.** Находка 6 — поправить B.1 в `implementation-plan.md` (с пометкой, что решение изменилось при реализации и почему), заодно перечитать `progress.md` на предмет утверждений, которые код уже опроверг. Не переписывать историю в CHANGELOG. **D6. Решение по `HEALTHCHECK`/`/healthz` (Opus).** Находка 9: либо строка в `Dockerfile` + описание, либо только описание эндпоинта. Влияет на код образа — поэтому отдельной задачей и не на Sonnet. **D7. Регресс-защита (см. раздел 5).** **D8. Новые документы: `architecture.md` и `development.md`.** Пишутся с нуля и перенимают техническую часть ТЗ (§4–7, §9, §12) в форму as-built / процесса. `architecture.md` сверяется методом из раздела 2 — по коду, не по памяти. `development.md` фиксирует воспроизводимый процесс (Go локально / e2e / dev-сервер). *Готово, когда:* `architecture.md` перечисляет все managed-процессы из [build/supervisord.conf](../build/supervisord.conf) и путь письма через milter-цепочку без противоречий коду; `development.md` позволяет с нуля повторить локальную сборку + прогон тестов и понять, когда нужен dev-сервер, не заглядывая в память проекта и не открывая `specification.md`. **D9. Вывод `specification.md` из обращения (после D1–D8).** Закрывает цель плана: живой ТЗ больше не нужен. 1. Создать `docs/product.md` — §1–3, §4.1 (см. карту миграции в §1). 2. Дополнить [security.md](security.md) разделом «Обязательные требования» — самодостаточный чеклист из бывшего §7.6 (setup-link, SASL, сессии, rate-limit логина, `html/template`, не-root и т.д.), без отсылки «см. ТЗ». 3. Убедиться, что D1–D4 и D8 покрыли всё из §8–11, что должно жить в README / `architecture.md` (пройти карту миграции построчно). 4. Перенести `specification.md` → `docs/archive/specification-v1.0.md` без правок текста; в начале архива — одна строка: «исторический снимок v1.0, не источник истины». 5. Обновить ссылки во всём репозитории: `progress.md` (убрать «ТЗ: specification.md», заменить на `product.md` + `architecture.md`), `implementation-plan.md` и `roadmap.md` («Основа» → `product.md`), `security.md`, `README.md` (баннер D4 — не на ТЗ). `rg specification\.md` по репо — только архив и CHANGELOG/история. *Готово, когда:* в `docs/` нет `specification.md`; новый агент после `/clear` может понять продукт, устройство, безопасность и процесс разработки, не открывая архив. --- ## 5. Чтобы не разошлось снова 1. **Правило шага:** изменение, добавляющее/переименовывающее env-переменную, маршрут панели или наблюдаемое поведение почтового тракта, закрывается только вместе с правкой README/`.env.example` — в том же коммите, наравне с записью в CHANGELOG (протокол закрытия шага в [progress.md](progress.md)). 2. **Дешёвая машинная проверка (D7):** тест или скрипт, сверяющий множество ключей `loadConfig` со списком, объявленным в документации, и падающий на новом недокументированном ключе. Ловит самый частый класс расхождений (находка 3) без ручного прохода. Границу «внутренних» ключей задать явным списком-исключением в самом тесте. 3. **Перед каждым тегом** — короткий проход по разделу 2 (источники истины в коде + README + `architecture.md` + `product.md`), а не полная ревизия текста и не сверка с архивным ТЗ. --- ## 6. Гейт релиза Документационный проход — часть того же гейта, что e2e (C.4) и ревизия безопасности (D.5): **D1–D6 и D9 закрыты до тега**. D7 желателен, но тег не блокирует. **D8** — обязателен до D9 (архитектура должна существовать до миграции); D8 один тег не блокирует, если D9 отложен, но **полное закрытие плана (= вывод specification) — только D1–D9 вместе**. Находки, обнаруженные позже, дописываются сюда, а не исправляются молча: этот файл — журнал состояния документации, а не одноразовый список дел.