f5f4bd4551
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>
249 lines
22 KiB
Markdown
249 lines
22 KiB
Markdown
# План документации SelfPost
|
||
|
||
**Зачем этот файл.** Документация — часть поставки (ТЗ раздел 11, пп. 5, 7, 9),
|
||
а не сопроводительный текст. Перед тегом релиза она обязана описывать **то, что
|
||
делает код**, а не то, что задумывалось: расхождение здесь — такой же дефект,
|
||
как несоответствие ТЗ, только обнаруживает его пользователь на своём проде.
|
||
План описывает, из чего состоит пакет, как он сверяется с кодом, что уже
|
||
разошлось (первый проход выполнен, результаты ниже) и что с этим делать.
|
||
|
||
**Место в общем плане:** документационный проход идёт вместе с **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, источник истины по требованиям.
|
||
[implementation-plan.md](implementation-plan.md) — открытые вопросы и линия 2.x.
|
||
[progress.md](progress.md) — живой трекер, читается первым после `/clear`.
|
||
[security.md](security.md) — принятые риски. Этот файл — план по документации.
|
||
|
||
**Границы:** отдельные `CONTRIBUTING.md`, `docs/architecture.md`, man-страницы и
|
||
сайт документации ТЗ **не требует** и в объём v1.x не входят. Dev-петля (сборка
|
||
и тесты на dev-сервере) остаётся в `progress.md` и в память проекта — выносить
|
||
её в поставляемую документацию не нужно.
|
||
|
||
---
|
||
|
||
## 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) |
|
||
| Требования, а не реализация (что вообще должно быть описано) | [specification.md](specification.md) разделы 9, 10, 11 |
|
||
|
||
Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты,
|
||
`postconf`-настройки), потом искать каждый пункт в README — так находится и
|
||
неверное, и **отсутствующее**.
|
||
|
||
---
|
||
|
||
## 3. Результаты первого прохода (выполнен)
|
||
|
||
Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI,
|
||
сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию
|
||
приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4.
|
||
|
||
**Высокий (пробел против ТЗ или вводит в заблуждение)**
|
||
|
||
1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7
|
||
([specification.md:427](specification.md)). В README нет ни слова про
|
||
`/status` (проверки PTR/hostname), `/sendlog` (журнал отправки, фильтры),
|
||
`/queue` (очередь Postfix), `/logtail` (хвост `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: баннер статуса и ссылки; комментарий
|
||
в шапке 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](progress.md)).
|
||
2. **Дешёвая машинная проверка (D7):** тест или скрипт, сверяющий множество
|
||
ключей `loadConfig` со списком, объявленным в документации, и падающий на
|
||
новом недокументированном ключе. Ловит самый частый класс расхождений
|
||
(находка 3) без ручного прохода. Границу «внутренних» ключей задать явным
|
||
списком-исключением в самом тесте.
|
||
3. **Перед каждым тегом** — короткий проход по разделу 2 этого файла (семь
|
||
источников истины), а не полная ревизия текста.
|
||
|
||
---
|
||
|
||
## 6. Гейт релиза
|
||
|
||
Документационный проход — часть того же гейта, что e2e (C.4) и ревизия
|
||
безопасности (D.5): **D1–D6 закрыты до тега**. D7 — желателен, но тег не
|
||
блокирует. Находки, обнаруженные позже, дописываются сюда, а не исправляются
|
||
молча: этот файл — журнал состояния документации, а не одноразовый список дел.
|