Files
selfpost/docs/progress.md
T
mix 995bd5db84 docs: D6-D9 — HEALTHCHECK, env regression test, new docs, archive spec
Add Docker HEALTHCHECK and mail-path /healthz liveness; env-doc regression
test; architecture.md and development.md; product.md and expanded security.md;
retire live specification.md to docs/archive/.

Co-Authored-By: Claude <claude-opus-5-thinking-high@noreply@anthropic.com>
2026-08-05 00:33:49 +03:00

17 KiB
Raw Blame History

Прогресс реализации SelfPost

Живой трекер состояния. Переживает /clear — читается первым при возобновлении работы. План (открытые вопросы для v1.0/v1.x): implementation-plan.md. Линия 2.x.x (входящий релей, роль администратора домена): roadmap.md. Продукт: product.md, устройство: architecture.md. Процесс разработки: development.md. Принятые риски безопасности: security.md. История релизов: CHANGELOG.md. История сделанного по фазам (0→13, все закрыты) — в git log и в CHANGELOG, здесь не дублируется.

Как возобновить после сброса контекста

  1. Прочитать этот файл (текущее состояние, что дальше).
  2. Открыть implementation-plan.md — там нерешённые вопросы для v1.0/v1.x; линия 2.x.x (Фаза O1+, роль администратора домена) — в roadmap.md; принятые риски безопасности — в security.md.
  3. При необходимости — architecture.md и product.md.
  4. Продолжить с пункта «Следующий шаг».

Модель по типу работы

Правило: безопасность / инфра / риск-критичное → Opus; UI / документация / бойлерплейт → Sonnet; тривиальная механика → Haiku.

Исключение — ревизия (не написание) кода: предрелизная проверка на уязвимости (пункт D.5 плана) делается моделью Fable, чтобы проверял не тот, кто писал.

Коммиты

Коммит на каждом осмысленном шаге (не каждое сохранение файла, но и не только конец фазы): рабочий под-функционал, зелёная сборка, конец фазы. Минимум — один коммит на закрытую фазу + промежуточные на связные под-шаги. Ветка main (если пользователь не попросит отдельную). Push/PR — только по явной команде. Сообщение коммита завершается трейлером Co-Authored-By: Claude <модель> <noreply@anthropic.com> — с той моделью, которая этот шаг делала (на момент Фазы 14 — Claude Opus 5).

На каждом таком шаге — запись в CHANGELOG.md под [Unreleased] (формат Keep a Changelog). При явном решении зарезать версию — секция [Unreleased] переименовывается в [X.Y.Z] - дата, заводится новая пустая [Unreleased]. Тег/пуш образа — только по явному запросу (см. workflow release.yml).

Протокол закрытия фазы/крупного шага

Перед /clear в конце каждого законченного шага Claude:

  1. Обновляет этот файл: «Текущее состояние» → что изменилось, что дальше.
  2. Проверяет применимые критерии «Готово, когда…».
  3. Дописывает CHANGELOG.md под [Unreleased].
  4. Делает финальный коммит шага.

Текущее состояние

  • Выполнено и принято: базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница /status, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie __Host- + обнаружение дублей, документация про /data/setup-token). Что именно сделано — в git log и CHANGELOG.md, здесь не дублируется.
  • B.1 реализован (не выкачен на прод): сессии переехали в SQLite (internal/store/migrations/0002_sessions.sql, internal/store/sessions.go, internal/web/session.go) — хранится SHA-256 токена, не сам токен; скользящий срок бездействия PANEL_SESSION_IDLE_DAYS (по умолчанию 7 дней, без абсолютного потолка); запись в БД продлевается не чаще раза в час (renewThreshold); опросы мониторинга (GET с HX-Request) продление не триггерят (isSessionActivity в internal/web/middleware.go); Max-Age cookie выставляется тем же значением при логине и при продлении (setSessionCookie); смена пароля разлогинивает все сессии кроме текущей (уже было, теперь через БД). Проверено на стенде: логин → рестарт процесса панели → сессия жива по старой cookie; HX-Request-опрос и повторный GET внутри часового окна не шлют Set-Cookie. go vet/go test ./.../gofmt -l . чистые.
  • B.2 реализован (не выкачен на прод): ротация mail.log ушла с copytruncate на «переименовать + postfix reload» — build/logrotate-mail.conf (nocreate заменён на create 0644 root root не по плану, а по стендовой проверке: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом 0600, недоступным непривилегированной панели, — create в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); follow() в internal/logtail/logtail.go при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; readLogTail() в internal/web/handlers_monitor.go считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (selfpost.mixfed.ru, отдельный контейнер selfpost:b2test2): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает mail.log сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. go vet/go test ./.../gofmt -l . чистые (на dev-сервере; локально на Windows TestFollowTailsAndRotates падает — rename открытого файла запрещён ОС, к делу не относится).
  • B.3 реализован (не выкачен на прод): build/entrypoint.sh проверяет SELFPOST_HOSTNAME до postfix-config.sh и до supervisord — при пустом значении exit 1 с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через case: минимум одна точка, без схемы/порта/пробелов (*://*, *:*, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). saslRealm() и fallback в postfix-config.sh не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в README.md и deploy/.env.example. Проверено на стенде (selfpost.mixfed.ru, отдельный образ selfpost:b3test, cap-list как в поставляемом compose): без переменной — exit 1 с ожидаемым текстом, без бесконечного тихого retry; https://mail.example.com:465 и localhost отклонены с понятными сообщениями; валидный mail.example.com — обычный старт, все процессы supervisord поднимаются. go vet/go test ./... чистые.
  • C.4 реализован (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем test/e2e/ (свой go.mod, не подхватывается go test ./... основного модуля) поверх поставляемого deploy/docker-compose.yml плюс test/e2e/compose.override.yml (самоподписанный сертификат, PANEL_COOKIE_SECURE=false, SELFPOST_HOSTNAME=mail.e2e.test, высокие порты 20465/20587/20080, изолированный compose-проект selfpost-e2e, свой --project-directory — прод на том же хосте не задет). Герметичная почта: CoreDNS (test/e2e/dns/Corefile — авторитетна только для e2e.test, file-плагин с саб-директивой reload перечитывает db.zone по mtime, без сигналов) плюс smtp-sink из пакета postfix (test/e2e/sink/) как sink-MX. Сценарий (test/e2e/*_test.go): старт контейнера → все supervisord-программы RUNNING (postfix-reloadSTOPPED) → токен из /data/setup-token → setup → login → добавление домена → DKIM-запись скраплена со страницы панели и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (go-msgauth/dkim с кастомным LookupTXT через CoreDNS) против ключа из DNS, не из панели напрямую → send-log queued → sent. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (reject_sender_login_mismatch репортится Postfix'ом на RCPT, не MAIL — smtpd_delay_reject=yes по умолчанию), L1-лимит (anvil, override RATE_LIMIT_MESSAGES_PER_IP=50 — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → rejected-строка в send-log), fail-open journal-milter'а (supervisorctl stop panel, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный SELFPOST_HOSTNAME (отдельный один-разовый контейнер, не общий стенд), сессия переживает docker restart (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). make e2e — локальный/dev-server прогон. Найдено и исправлено по ходу стендовой проверки: reload — саб-директива file-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел reload следит за самим Corefile, не за зоной); docker compose build.context резолвится относительно --project-directory, а не относительно файла, где объявлен; smtp-sink отказывается стартовать от root без -u; html/template эскейпит + в &#43; даже в тексте — скрапер значений со страницы обязан html.UnescapeString; проверки состояния сразу после up/restart должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). Проверено на dev-сервере (selfpost.mixfed.ru): make e2e — зелёный (go vet/gofmt -l тоже чистые в обоих модулях). release.yml переработан: job prepare (версия из тега) → матрица [ubuntu-latest, ubuntu-24.04-arm] — каждая нативно собирает образ (--load), прогоняет e2e, пушит тег X.Y.Z-amd64/X.Y.Z-arm64 → job mergedocker buildx imagetools create в единый тег X.Y.Z; setup-qemu-action убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному make e2e пути.
  • Документация (D1–D9 закрыты): documentation-plan.md — D6 HEALTHCHECK//healthz, D7 env-doc regression test, D8 architecture.md/development.md, D9 вывод specification.md (архив в docs/archive/, живые документы product.md + расширенный security.md). Часть предрелизного гейта наравне с e2e и ревизией безопасности.
  • Дальше: пункт D.5 плана — предрелизная проверка на уязвимости моделью Fable по всему дифу от v1.0.0 плюс повторный проход по ТЗ 7.6; вместе с e2e (C.4, готов) это гейт перед тегом релиза.
  • Дальше — то, что перечислено в implementation-plan.md: открытые вопросы закрыты, раздел E теперь только указатель на объём 2.x (входящий релей O1+ и роль администратора домена; 2FA снята с рассмотрения); остаются принятые риски безопасности (переехали в security.md: POST без Sec-Fetch-Site/Origin пропускается, CSRF-токенов нет) и опциональная Фаза O1+ (входящий релей, линия 2.x.x, требует согласования).
  • Прод: selfpost.mixfed.ru, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного Host (Apache-фрагмент из deploy/ это делает).

Рабочая петля (dev loop) — ВАЖНО

Локально (Windows, D:\Local\Git\selfpost) нет Go и Docker — только редактирование и git. Вся сборка/тесты идут на dev-сервере selfpost.mixfed.ru (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → залить дерево на сервер → go build/go vet/docker build/тесты там. rsync в локальном git-bash нет, поэтому дерево едет tar'ом по ssh: tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru '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.mixfed.ru (по ключу).