diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e7936..278b4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version live document that owns the subject — `docs/architecture.md` (with section), `docs/product.md`, `docs/security.md`, or the README. Comments only; no behaviour is affected. +- `docs/code-review.md` is gone. Its plan is finished — phases 0 (bar the + release-commit steps), 1, 1.5, 2 and 3 are all closed — and the rest of the + document had become a second copy of what `architecture.md`, `security.md` + and the code comments already say. What was genuinely open moved to + `docs/roadmap.md`: splitting `internal/web` into subpackages, a consolidated + documentation index in the README, the adaptive polling interval for an idle + but visible tab, and `CONTRIBUTING.md`. The review text stays in git history + (`522425a`); the CHANGELOG entries below that cite it are left as written. - `docs/architecture.md` gained a *Code layers* section: a diagram of handlers → services → store plus the adapters, and the reason the services layer exists (multi-store writes and their rollback) — closing item A2 of diff --git a/docs/code-review.md b/docs/code-review.md deleted file mode 100644 index 88d5d9e..0000000 --- a/docs/code-review.md +++ /dev/null @@ -1,472 +0,0 @@ -# Рецензирование кодовой базы SelfPost - -**Дата:** 2026-08-05 -**Объём ревью:** ~172 файла, 64 Go-исходника (~10 770 строк в `internal/` + `cmd/`), 29 unit-тестов, e2e-модуль `test/e2e/`, 17 HTML-шаблонов, 15 doc-файлов. - -Связанные документы: [architecture.md](architecture.md), [security.md](security.md), [implementation-plan.md](implementation-plan.md), [progress.md](progress.md), [roadmap.md](roadmap.md). - ---- - -## Общая оценка - -| Критерий | Оценка | Комментарий | -|----------|--------|-------------| -| Архитектура | **Отлично** | Чёткое слоение, минимум связности | -| Сложность vs масштаб | **Отлично** | Без over-engineering | -| Качество кода | **Отлично** | Идиоматичный Go, продуманные rollback-пути | -| Документация | **Хорошо** | As-built docs точны; есть RU/EN split и stale comments | -| Поддерживаемость | **Хорошо** | Высокий порог входа из-за phase-комментариев | -| Логические ошибки | **Минимально** | Критичных багов не найдено; есть принятые операционные gap'ы | -| Legacy | **Низкий** | Только архив spec + phase-комментарии | -| GUI | **Хорошо** | Нет «костылей»; осознанные CSP/layout компромиссы | - -**Вывод:** проект готов к релизному тегу после закрытия § D ([implementation-plan.md](implementation-plan.md)) — предрелизного security review. Остальное — polish, не блокеры. - -**Статус на 2026-08-06:** § D закрыт, фазы 1, 1.5, 2 и 3 выполнены, добор по §§ 1/3/4 сделан. Незакрытым остаётся только то, что делается в момент резки версии: бамп тега образа в compose и сам git-тег ([roadmap.md](roadmap.md) § v1.x). - ---- - -## 1. Архитектура и структура проекта - -### Текущая структура - -```mermaid -flowchart TB - subgraph cmd [cmd] - panel["panel (HTTP + milter + logtail)"] - backup["selfpost-backup CLI"] - end - subgraph web [internal/web — 3030 LOC] - handlers["handlers_*.go"] - templates["templates/*.html"] - security["security.go, session.go"] - end - subgraph services [Services] - domainSvc["internal/domain"] - appSvc["internal/app"] - end - subgraph persistence [Persistence] - store["internal/store (SQLite)"] - end - subgraph adapters [Adapters] - postfix["internal/postfix"] - milterPkg["internal/milter"] - logtail["internal/logtail"] - dnscheck["internal/dnscheck"] - backupPkg["internal/backup"] - health["internal/health"] - end - panel --> web - panel --> milterPkg - panel --> logtail - web --> domainSvc - web --> appSvc - domainSvc --> store - appSvc --> store - milterPkg --> store - logtail --> store - domainSvc --> postfix - appSvc --> postfix -``` - -### Сильные стороны - -- **Layered / ports-and-adapters:** handlers → services (`domain`, `app`) → `store`; инфраструктура изолирована в адаптерах ([`internal/web/web.go`](../internal/web/web.go), [`internal/app/service.go`](../internal/app/service.go)). -- **Composition root** в [`cmd/panel/main.go`](../cmd/panel/main.go): три роли (HTTP, journal-milter, log-tailer) в одном процессе — оправдано для single-container deployment. -- **Interface seams** для тестов: `milter.Store`, `app.SenderMaps`, `logtail.StatusStore`. -- **Embedded migrations** ([`internal/store/store.go`](../internal/store/store.go)) — простой, надёжный подход для 3 миграций. -- **E2E как отдельный модуль** (`test/e2e/go.mod`) — не загрязняет основной модуль. - -### Замечания (не блокеры) - -- **`internal/web/` — 47 файлов, 3030 строк** — самый крупный пакет. При росте v2.x (роли, inbound relay) стоит выделить подпакеты (`web/handlers`, `web/auth`) или split по доменам. Сейчас — приемлемо. -- **Один SQLite connection** (`MaxOpenConns(1)`) — сознательный trade-off; при росте нагрузки milter + HTTP + logtail будут сериализованы. Документировано, для single-admin panel — норма. - -### Рекомендации - -| # | Действие | Приоритет | -|---|----------|-----------| -| A1 | Оставить текущую структуру; рефакторинг пакетов — только при старте 2.x | Низкий | -| A2 | Добавить в [architecture.md](architecture.md) диаграмму слоёв (как выше) — **выполнено** (§ Code layers) | Низкий | - -**Модель:** Sonnet (документация) - ---- - -## 2. Соответствие сложности масштабу проекта - -### Факты - -- Outbound SMTP relay + admin panel для одного оператора. -- ~10.7K строк Go, 3 зависимости (`go-milter`, `x/crypto`, `modernc.org/sqlite`). -- Нет ORM, нет SPA, нет message queue — всё уместно. - -### Сильные стороны - -- Нет лишних абстракций (нет generic repository, нет DI-фреймворка). -- Сервисный слой тонкий, но достаточный: координация SQLite + sasldb2 + Postfix maps ([`internal/app/service.go`](../internal/app/service.go)). -- Fail-open/fail-closed решения явно задокументированы (OpenDKIM tempfail vs journal fail-open). - -### Замечания - -- **Rate limiting в двух местах** (Postfix anvil L1 + milter L2) — сложность оправдана спецификацией, но требует понимания оператором. -- **Backup с manifest version gate** ([`internal/backup/backup.go`](../internal/backup/backup.go)) — чуть тяжелее минимума, но оправдано для migration safety. - -**Вердикт:** сложность **адекватна** масштабу. Over-engineering не обнаружен. - ---- - -## 3. Качество написанного кода - -### Сильные стороны - -- **Комментарии объясняют «почему»**, не «что» — образцовый уровень ([`internal/web/security.go`](../internal/web/security.go), [`internal/milter/milter.go`](../internal/milter/milter.go)). -- **Rollback-паттерны** при partial failure ([`internal/app/service.go`](../internal/app/service.go) `rollbackCreate`). -- **Ordering guarantees:** SQLite row before SASL write — защита от race и password clobber. -- **Validation centralized:** [`internal/web/validate.go`](../internal/web/validate.go), [`internal/app/validate.go`](../internal/app/validate.go). -- **Atomic file writes** для конфигов ([`internal/postfix/write.go`](../internal/postfix/write.go), [`internal/domain/dkim.go`](../internal/domain/dkim.go)). -- **Test coverage ~45%** file ratio (29 test / 64 source files); ключевые пути покрыты (auth, sessions, milter, backup, dnscheck). - -### Замечания - -| Файл | Замечание | Severity | -|------|-----------|----------| -| [`internal/web/token.go`](../internal/web/token.go) | `panic` при сбое `crypto/rand` — осознанно, документировано | Info | -| [`internal/web/handlers_domains.go`](../internal/web/handlers_domains.go) | Stale comment: «Applications and send log arrive in later phases» — **закрыто** (Фаза 1) | Low | -| Phase/spec references | ~50+ файлов с «Phase N», «spec 7.x» — **закрыто**: «Phase N» в Фазе 1, ссылки на архивную спецификацию — отдельным проходом (см. § 4) | Low | -| [`cmd/panel/main.go`](../cmd/panel/main.go) | Package comment всё ещё упоминает «Phase 1 stubs» — **закрыто** (Фаза 1) | Low | - -### Потенциальные улучшения качества - -- Единый проход **gofmt + удаление stale phase-комментариев** (механическая работа) — **выполнено** (Фаза 1; `gofmt -l` теперь и в CI). -- Добавить **table-driven test** для edge cases в `parseDelivery` (exotic Postfix status values) — **выполнено**, и проход оказался не косметическим: он вскрыл реальный баг. Шаблон разбора брал `status=` жадно, то есть **последнее** вхождение в строке, а Postfix дописывает в конец ответ удалённого сервера дословно. Отказ, в тексте ответа которого встречалось `status=sent`, попадал в журнал как доставленный. Исправлено на ленивый разбор (первое `status=` после получателя). - -**Модель:** Haiku (механическая чистка комментариев), Sonnet (точечные правки) - ---- - -## 4. Полнота документации и соответствие коду - -### Сильные стороны - -- **[architecture.md](architecture.md)** — as-built source of truth; маршруты, процессы, persistence совпадают с кодом. -- **[security.md](security.md)** — чеклист + принятые риски; каждый риск привязан к коду. -- **[product.md](product.md)** — границы v1.0/out-of-scope чёткие. -- **Regression guard:** [`cmd/panel/envdoc_test.go`](../cmd/panel/envdoc_test.go) — env vars в README = `loadConfig`. -- **CHANGELOG** в формате Keep a Changelog. - -### Расхождения docs ↔ code - -| Проблема | Где | Реальность | -|----------|-----|------------| -| Image tag `0.1.0` vs «v1.0» | [`deploy/docker-compose.yml`](../deploy/docker-compose.yml) vs README | **Открыто:** roadmap § v1.x — bump в релизном коммите вместе с git-тегом | -| Quick start URLs | README | **Закрыто:** Codeberg уходит как публичная площадка, единственный дом проекта — GitHub; вместе с URL переехал и путь Go-модуля (`github.com/mixeme/selfpost`) | -| `docs/logo` | roadmap | **Закрыто** (Фаза 1): каталог отсутствует, критерию удовлетворяет | -| `docs/specification.md` | documentation-plan D9 | **Закрыто:** файл остаётся в `docs/archive/` как история, но ссылок на него из кода больше нет — все «spec N.x» заменены на живые документы | -| Phase language в коде | 50+ файлов | **Закрыто** (Фаза 1) | -| RU/EN split | progress, roadmap, implementation-plan (RU) vs README/architecture (EN) | Намеренно, но барьер для EN-only contributors; снимается вместе с `CONTRIBUTING.md` — перенесено в [roadmap.md](roadmap.md) § 2.x | - -### Комментирование кода - -- **Высокое качество** в security-critical paths. -- **Среднее** в CRUD handlers (делегируют в services — acceptable). -- **Выполнено:** ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», но и «spec 4/5/6/8/9», которые страдали ровно тем же (указывали в документ, помеченный «не источник истины»). Каждая заменена на живой документ, владеющий темой: [architecture.md](architecture.md) с указанием секции, [product.md](product.md), [security.md](security.md) или README. Секция указывается там, где документ большой (architecture.md, README); для короткого `product.md` — только файл. - -**Модель:** Sonnet (docs sync) - ---- - -## 5. Читаемость и поддерживаемость - -### Для кого код читаем - -- **Go-разработчик со знанием SMTP/Postfix** — да, без проблем. -- **Новичок без почтового бэкграунда** — потребуется [architecture.md](architecture.md) + README. - -### Факторы, помогающие поддержке - -- Предсказуемая структура handler → service → store. -- Embedded templates (`//go:embed`) — один binary, нет внешних assets. -- Makefile targets: `vet`, `test`, `build`, `e2e`. -- E2E suite покрывает happy path + negatives. - -### Факторы, затрудняющие поддержку - -- Phase-номера в комментариях без контекста. -- Dual cookie names (`__Host-` vs plain) — хорошо документировано, но неочевидно. -- HTMX fragment polling — нужно понимать SSR + partial updates. -- Dev loop: Windows local edit → SSH to Debian server ([progress.md](progress.md)) — не стандартный `go run`. - -### Рекомендации - -| # | Действие | Модель | -|---|----------|--------| -| M1 | Cleanup phase-комментариев → «as-built» language | Haiku | -| M2 | Добавить `CONTRIBUTING.md` (dev loop, model routing, commit protocol) — опционально v1.x | Sonnet | -| M3 | Consolidated doc index в README (ссылки на все docs/) | Sonnet | - ---- - -## 6. Логические ошибки и риски - -### Критичных багов не обнаружено - -E2E покрывает: bootstrap, SMTP AUTH, DKIM, send-log lifecycle, negatives (relay, sender mismatch, L1/L2 limits, milter fail-open, hostname gate, session survive restart). - -### Принятые операционные gap'ы (не баги, но важно знать) - -| Gap | Описание | Документировано | -|-----|----------|-----------------| -| Send-log `queued` forever | **Закрыто для рестарта:** Фаза 3 — offset персистится (`logtail_state`), хвост дочитывается. Остаётся пересоздание контейнера: `mail.log` не в `/data` | [security.md](security.md), [roadmap.md](roadmap.md) | -| CSRF without tokens | POST без Origin/Sec-Fetch-Site пропускается | [security.md](security.md) | -| Fail-open L2 rate limit | DB error → mail проходит | [`internal/milter/ratelimit.go`](../internal/milter/ratelimit.go) | -| Shallow SPF check | Не следует `include:`/`redirect=` | README, `internal/dnscheck/spf.go` | -| Plaintext backup/export at rest | DKIM-ключи, SASL, пароли приложений в cleartext `.tar.gz`/`.json` | **Закрыто:** R13 — опциональное шифрование (`.spbk`/`.spde`); открытый вариант остаётся умолчанием, риск переформулирован в [security.md](security.md) | - -**Не риск (решение оператора):** «Session resurrection from backup» — снято из [security.md](security.md). - -### Потенциальные логические нюансы (низкий приоритет) - -1. **Rate limit race:** `CountMessages` + `InsertQueued` не в одной транзакции — при высокой нагрузке возможен overshoot на 1 сообщение. Для differentiated limits — acceptable. -2. **Domain export с plaintext passwords** ([`internal/domain/transfer.go`](../internal/domain/transfer.go)) — by design; **mitigation:** R13 (optional password encryption). -3. **`macro()` dual lookup** ([`internal/milter/milter.go`](../internal/milter/milter.go)) — workaround для Postfix/go-milter; permanent, not a bug. - -### Рекомендации - -| # | Действие | Приоритет | Модель | -|---|----------|-----------|--------| -| L1 | **Предрелизный security review** (§ D) — обязательный гейт | **P0** | **Fable** | -| L2 | **Шифрование бэкапа и экспорта домена** (R13) — optional, checkbox + password — **выполнено** | P1 | **Opus** + Sonnet | -| L3 | Send-log gap mitigation — **выполнено** (persist offset, Фаза 3) | P2 | Opus | -| L4 | Rate limit count+insert — **выполнено** (учёт «в полёте», Фаза 3; транзакция как таковая неприменима) | P3 | Opus | - ---- - -## 7. Legacy-код и миграции - -### SQL-миграции - -- [`0001_init.sql`](../internal/store/migrations/0001_init.sql) — initial schema -- [`0002_sessions.sql`](../internal/store/migrations/0002_sessions.sql) — sessions (plan B.1) -- [`0003_logtail_state.sql`](../internal/store/migrations/0003_logtail_state.sql) — log-tailer read offset (Фаза 3) -- Механизм: `PRAGMA user_version`, embedded FS, transactional apply — **чистый**, без legacy branches в коде. - -### Архивная документация - -- [`docs/archive/specification-v1.0.md`](archive/specification-v1.0.md) — historical; помечен «не источник истины». -- **Рекомендация:** оставить в archive; в коде заменить «spec 7.x» на актуальные doc-ссылки. - -### Legacy patterns в runtime - -| Элемент | Статус | Действие | -|---------|--------|----------| -| Phase 0–14 comments | Historical noise | Cleanup (Haiku) | -| `macro()` brace workaround | Permanent Postfix compat | Оставить, уже документировано | -| Legacy charset (windows-1251) in milter | Keep raw header on decode fail | Оставить | -| In-memory sessions | **Удалено** (B.1 → SQLite) | Done | -| `copytruncate` log rotation | **Заменено** (B.2 → rename+reload) | Done | - -**Перспектива удаления:** единственный кандидат на cleanup — **phase-комментарии** и **архив spec** (оставить файл, убрать ссылки из кода). - ---- - -## 8. GUI: «костыли» и оптимизация компоновки - -### Стек - -Go `html/template` + HTMX polling + [`panel.css`](../internal/web/static/panel.css) + [`panel.js`](../internal/web/static/panel.js). **Нет React/Vue** — минимальный footprint. - -### Поиск маркеров долга - -**TODO / FIXME / HACK / kostyl — 0 вхождений** по всему репозиторию. - -### Осознанные компромиссы (не костыли) - -| Компромисс | Файл | Обоснование | -|------------|------|-------------| -| External CSS/JS only (no inline) | `panel.css`, `panel.js` | CSP `default-src 'self'` | -| HTMX `includeIndicatorStyles: false` | `layout.html` | Avoid CSP exception | -| Block layout for applications (not table) | `panel.css` | 4 cols + 6 controls don't fit 48rem | -| Two-row nav | `panel.css` | Session block vs page links width | -| Page-specific max-width (48/64/24rem) | `panel.css` | Monitoring vs forms | -| Subject ellipsis via inner `` | `panel.css` | `max-width` on `` is advisory | -| Dark mode `!important` overrides | `panel.css` | Override specificity without restructuring | -| HTMX poll excluded from session renewal | `middleware.go` | Idle timeout semantics | -| Dual cookie names | `handlers_auth.go` | `__Host-` requires Secure | - -### Возможные оптимизации GUI - -| # | Оптимизация | Effort | Модель | -|---|-------------|--------|--------| -| G1 | HTMX polling only when tab visible (`document.visibilityState`) | Low | Sonnet | -| G2 | CSS custom properties для dark mode вместо `!important` cascade | Medium | Sonnet | -| G3 | Consolidate duplicate `main { max-width }` rules | Trivial | Haiku | -| G4 | `hx-trigger="every 5s"` → adaptive interval (5s active, 30s idle) | Low | Sonnet | - -**Вердикт:** GUI **не содержит костылей**; все workarounds документированы и оправданы CSP/layout constraints. - ---- - -## 9. Слабо задокументированные спорные решения - -### Хорошо задокументированные (security.md + code comments) - -- Fail-open journal-milter vs fail-closed OpenDKIM -- CSRF via Origin (no tokens) -- `__Host-` cookie + duplicate detection -- Plaintext passwords in domain export → **mitigation:** R13 -- Send-log queued gap -- SQLite single connection -- No in-container TLS - -### Требуют усиления документации - -| Решение | Текущее состояние | Рекомендация | -|---------|-------------------|--------------| -| **Почему нет CSRF-токенов** (только Origin check) | Частично в security.md | Добавить ADR-style параграф в security.md | -| **Почему panel HTTP, не HTTPS** | README + architecture | Достаточно | -| **Почему chroot disabled в Postfix** | architecture.md | Достаточно | -| **Порядок supervisord** (opendkim → panel → postfix) | architecture.md | Достаточно | -| **Почему log-tailer не persist offset** | roadmap optional | Явно в architecture.md § known limitations | -| **Import domain с plaintext password** | transfer.go comment | Достаточно для v1; шифрование — R13 | -| **Plaintext full backup / domain export at rest** | handlers_backup.go | **R13:** optional AES-GCM envelope (`.spbk`/`.spde`) | - -**Модель:** Sonnet (дополнить security.md / architecture.md) - ---- - -## 10. Прочие предложения по оптимизации - -### Pre-release (блокеры) - -| # | Задача | Модель | Ref | -|---|--------|--------|-----| -| R0 | Security review diff v1.0.0→HEAD + checklist 7.6 — **выполнено** (2026-08-06) | **Fable** | implementation-plan § D | -| R1 | Bump image tag in compose при git tag — **открыто**, делается в релизном коммите вместе с тегом | Sonnet | roadmap § v1.x | -| R2 | ~~Codeberg URLs в README Quick start~~ — **снято**: Codeberg уходит, GitHub остаётся единственной площадкой. Вместо перевода ссылок *на* Codeberg сделан обратный переезд: URL, лицензионные шапки SVG/HTML и путь Go-модуля | Sonnet | — | - -### v1.x polish (не блокеры) - -| # | Задача | Модель | -|---|--------|--------| -| R3 | Cleanup phase-комментариев (50+ files) — **выполнено** (Фаза 1) | Haiku | -| R4 | Fix stale comment in handlers_domains.go — **выполнено** (Фаза 1) | Haiku | -| R5 | docs/logo: создать или удалить из roadmap — **выполнено** (Фаза 1) | Haiku | -| R6 | GUI: visibility-aware HTMX polling — **выполнено** (Фаза 2) | Sonnet | -| R7 | CONTRIBUTING.md — **перенесено в 2.x** ([roadmap.md](roadmap.md)): у проекта один разработчик и нет внешнего потока PR, документ был бы без аудитории | Sonnet | -| R8 | ADR для CSRF policy — **выполнено** (Фаза 1, [security.md](security.md)) | Sonnet | -| R13 | Шифрование бэкапа и экспорта домена (checkbox + password) — **выполнено** | **Opus** + Sonnet | - -### v2.x (roadmap, не начинать без согласования) - -| # | Задача | Модель | -|---|--------|--------| -| R9 | Inbound relay (Phase O1) | **Opus** | -| R10 | Domain-admin role | Opus | -| R11 | Send-log gap fix (persist offset) — **выполнено в Фазе 3**, из 2.x снято | Opus | -| R12 | Split internal/web subpackages | Sonnet/Opus | - -### CI/infra - -- E2E готов (`test/e2e/`); release workflow matrix amd64/arm64 — **хорошо**. -- `go vet` + `go test` на push — **достаточно** для v1.x. -- Рекомендация: добавить `gofmt -l` check в CI (progress.md упоминает как manual step) — **выполнено** (Фаза 1, [.github/workflows/test.yml](../.github/workflows/test.yml)). - -**Модель:** Haiku (CI one-liner) - ---- - -## План реализации (приоритизированный) - -### Фаза 0 — Гейт релиза (P0) - -Содержательная часть закрыта 2026-08-06; остались только шаги самой резки -версии, которые делаются по явной команде оператора. - -1. Fable: `/security-review` по diff v1.0.0...HEAD — **выполнено** -2. Fable: ручной проход security.md checklist § 7.6 — **выполнено** -3. Каждая finding → fix ИЛИ запись в security.md — **выполнено** (одна правка - defence-in-depth, принятые риски не пополнились) -4. `make e2e` зелёный — **выполнено** (dev-сервер) -5. ~~Codeberg URLs~~ — **снято**, см. R2: переезд сделан в обратную сторону, на - GitHub. Остаётся bump тега образа в compose — **открыто**, в релизном коммите -6. Git tag vX.Y.Z — **открыто**, [roadmap.md](roadmap.md) § v1.x - -### Фаза 1.5 — Шифрование резервных копий (P1, v1.x) — **выполнено 2026-08-06** - -Реализовано как спланировано: `internal/secretfile` (E1) → `selfpost-backup` -+ панель (E2) → экспорт/импорт домена (E3) → UI-чекбокс (E4) → docs (E5). -Отличия от плана: конверт потоковый (64 KiB чанки AES-256-GCM с AAD -`header+counter+last`), а не одноблочный, иначе полный бэкап пришлось бы -держать в памяти целиком; манифест остался внутри tar, то есть внутри -шифротекста, как и планировалось; в CLI добавлен режим `-decrypt` — без него -зашифрованный бэкап нечем распаковать при restore. Детали — -[progress.md](progress.md), CHANGELOG `[Unreleased]`. - -**Проблема:** полный бэкап и экспорт домена содержат DKIM-ключи, SASL-креды и plaintext-пароли приложений; сейчас `.tar.gz` / `.json` без шифрования. - -**Решение:** опциональное шифрование паролем (чекбокс «Encrypt with password»; поля password + confirm — только при включённой галочке; переключение в `panel.js`, без inline script). - -| Арtefact | Cleartext | Encrypted | -|----------|-----------|-----------| -| Полный бэкап | `.tar.gz` | `.spbk` (**S**elf**P**ost **B**ac**k**up) | -| Экспорт домена | `.json` | `.spde` (**S**elf**P**ost **D**omain **E**xport) | - -**Формат:** magic `SELFPOST1`, type byte, scrypt KDF, AES-256-GCM; manifest внутри ciphertext. - -**Задачи:** E1 crypto envelope → E2 backup/CLI → E3 domain export/import → E4 UI (checkbox) → E5 docs + e2e. **Модель:** Opus (crypto), Sonnet (UI/docs). - -### Фаза 1 — Doc/code hygiene (P1) — **выполнено 2026-08-06** - -1. Haiku: массовая замена phase-комментариев (mechanical pass) — **сделано** -2. Haiku: fix handlers_domains.go stale comment — **сделано** -3. Sonnet: ADR CSRF в security.md — **сделано** -4. Sonnet: known limitations § в architecture.md (send-log gap) — **уже было** - в § Log tailer, правка не потребовалась -5. Haiku: docs/logo resolve — **сделано** -6. Haiku: gofmt CI check — **сделано** - -Добор той же фазы (2026-08-06, отдельным проходом): ссылки на архивную -спецификацию убраны из кода целиком (§ 4), добавлена диаграмма слоёв в -architecture.md (A2), расширен `TestParseDelivery` (§ 3) — последнее вскрыло -реальный баг разбора `status=`. - -### Фаза 2 — GUI polish (P2, optional) — **выполнено 2026-08-06** - -1. Sonnet: HTMX visibility-aware polling (panel.js) — **сделано иначе**: - фильтр повешен на `htmx:beforeRequest`, а не на встроенный фильтр триггера - htmx — тот вычисляется через `new Function`, что CSP панели без - `unsafe-eval` молча ломает -2. Sonnet: CSS custom properties для dark mode — **сделано** -3. Haiku: consolidate main max-width rules — **сделано** - -### Фаза 3 — Operational improvements (P2–P3, optional) — **выполнено 2026-08-06** - -1. Opus: send-log read offset persistence — **сделано**: `logtail_state` - (миграция `0003`) хранит offset + отпечаток головы лога; при совпадении - отпечатка чтение продолжается, при несовпадении файл читается с начала, - первый запуск (записи нет) — с конца, как раньше. -2. Opus: rate limit count transaction wrap — **сделано иначе**: буквальная - транзакция невозможна, count живёт на MAIL FROM, insert — на end-of-message, - это разные стадии SMTP-транзакции. Overshoot закрыт учётом сообщений «в - полёте» (`internal/milter/inflight.go`): к счёту из БД добавляются - резервации, взятые прошедшими проверку сессиями и снимаемые после записи в - send-log, на ABORT или по TTL 10 минут. - -Остаток по send-log (не закрывается персистом offset): при пересоздании -контейнера `mail.log` теряется вместе с ним — принятый риск в -[security.md](security.md). - ---- - -## Маршрутизация моделей (сводная таблица) - -| Тип работы | Модель | Обоснование | -|------------|--------|-------------| -| Security review (не authorship) | **Fable** | Независимость от автора (Opus) | -| Security fixes, infra, Postfix | **Opus** | Risk-critical | -| UI, docs, CSS, templates | **Sonnet** | Баланс качества и скорости | -| Mechanical cleanup, CI, trivial fixes | **Haiku** | Минимальный scope | -| Inbound relay 2.x | **Opus** | Open relay risk | - -*Источник правил:* [progress.md](progress.md) § «Модель по типу работы», [development.md](development.md) § Agent rules. diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index e094d96..eb8c3c2 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -4,9 +4,8 @@ безопасности (§ D) выполнена 2026-08-06, релизный гейт (e2e + ревизия) открыт. B.1–B.3 и C.4 закрыты — as-built в [architecture.md](architecture.md), e2e/CI в [development.md](development.md), принятые риски в -[security.md](security.md). Комплексное рецензирование кодовой базы и план -доработок — [code-review.md](code-review.md). Текущее состояние и следующий -шаг: [progress.md](progress.md). Объём 2.x.x — [roadmap.md](roadmap.md). +[security.md](security.md). Текущее состояние и следующий шаг: +[progress.md](progress.md). Объём 2.x.x — [roadmap.md](roadmap.md). **Основа:** [product.md](product.md) v1.0. diff --git a/docs/progress.md b/docs/progress.md index 82f3ad1..df140ac 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -5,7 +5,6 @@ Линия 2.x.x (входящий релей, роль администратора домена): [roadmap.md](roadmap.md). Продукт: [product.md](product.md), устройство: [architecture.md](architecture.md). Процесс разработки: [development.md](development.md). Принятые риски безопасности: [security.md](security.md). -Рецензирование кодовой базы и план доработок: [code-review.md](code-review.md). История релизов: [CHANGELOG.md](../CHANGELOG.md). История сделанного по фазам (0→13, все закрыты) — в `git log` и в CHANGELOG, здесь не дублируется. @@ -48,14 +47,14 @@ - **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-reload` — `STOPPED`) → токен из `/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` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `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 `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути. - **Документация:** план D1–D9 закрыт ([documentation-plan.md](documentation-plan.md) — только метод и правила поддержки). Хвост v1.x — [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»; из него остался только бамп тега образа (Quick start и `docs/logo` закрыты). -- **Рецензирование кодовой базы** (2026-08-05): [code-review.md](code-review.md) — 10 разделов (архитектура, качество, docs, GUI, legacy, риски), приоритизированный план реализации и маршрутизация моделей. Критичных багов не найдено; блокер релиза — § D ниже. +- **Рецензирование кодовой базы** (2026-08-05, `522425a`): 10 разделов (архитектура, качество, docs, GUI, legacy, риски) плюс приоритизированный план доработок фазами 0–3. Критичных багов не найдено; единственным блокером релиза названа § D. **План выполнен целиком** (см. записи ниже), поэтому сам документ `docs/code-review.md` удалён — незакрытые пункты унесены в [roadmap.md](roadmap.md) (разбиение `internal/web`, индекс документации в README, адаптивный интервал опроса, `CONTRIBUTING.md`), остальное либо сделано, либо уже описано в architecture.md / security.md / комментариях кода. Текст ревизии — в git-истории. - **§ D выполнен (2026-08-06):** предрелизная ревизия безопасности моделью Fable — диф от аудита v1.0 (Фаза 11, `bd64e80`) до HEAD + полный проход по чек-листу [security.md](security.md) (бывшее ТЗ 7.6). Эксплуатируемых находок нет; одна правка defence-in-depth (`--` перед логином в argv `saslpasswd2`, `internal/app/sasl.go` + тест). Принятые риски не пополнились. Детали — [implementation-plan.md](implementation-plan.md) § D и CHANGELOG `[Unreleased]/Security`. Локально `go vet`/`go test ./internal/app/...` чистые; падения `internal/domain` (`TestWriteLoadPrivateKeyRoundtrip`, `TestRenderTables`) и `internal/logtail` (`TestFollowTailsAndRotates`) — Windows-специфика (права файлов/`\` в путях/rename открытого файла), на Linux CI зелено. -- **Фаза 1 выполнена (2026-08-06)** ([code-review.md](code-review.md) § Фаза 1 — doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях (dev-server). -- **Фаза 1.5 выполнена (2026-08-06)** ([code-review.md](code-review.md) § Фаза 1.5 — шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на dev-сервере. -- **Фаза 2 выполнена (2026-08-06)** ([code-review.md](code-review.md) § Фаза 2 — GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на dev-сервере. -- **Фаза 3 выполнена (2026-08-06)** ([code-review.md](code-review.md) § Фаза 3 — operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на dev-сервере. -- **Добор по code-review выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x» из § 4 ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) R7 (`CONTRIBUTING.md`) перенесён в 2.x, R1 и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально). -- **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа в compose, git tag) по явной команде пользователя. Все polish-фазы из [code-review.md](code-review.md) (1, 1.5, 2, 3) закрыты. +- **Фаза 1 плана ревизии выполнена (2026-08-06)** (doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях (dev-server). +- **Фаза 1.5 плана ревизии выполнена (2026-08-06)** (шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на dev-сервере. +- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на dev-сервере. +- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на dev-сервере. +- **Добор по плану ревизии выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», как просило ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) `CONTRIBUTING.md` перенесён в 2.x, бамп тега образа и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально). +- **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа в compose, git tag) по явной команде пользователя. Все polish-фазы плана ревизии (1, 1.5, 2, 3) закрыты. - **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена). - **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает). diff --git a/docs/roadmap.md b/docs/roadmap.md index 845cc09..6c346c5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -25,9 +25,9 @@ [deploy/docker-compose.yml](../deploy/docker-compose.yml) поле `image:` бампить до версии релиза **в том же коммите**, что и git-тег `vX.Y.Z` — не раньше. Сейчас там `0.1.0`, то есть отстаёт от целевой версии; несовпадение мешает -только до первого выката по тегу. Сам тег — последний шаг Фазы 0 -[code-review.md](code-review.md): содержательная часть гейта (e2e C.4, ревизия -§ D) закрыта, режется по явной команде оператора. После тега `release.yml` +только до первого выката по тегу. Сам тег — последний шаг релизного гейта: +содержательная часть (e2e C.4, ревизия § D) закрыта, режется по явной команде +оператора ([progress.md](progress.md)). После тега `release.yml` собирает и публикует `ghcr.io/mixeme/selfpost:X.Y.Z`, поэтому compose с новым тегом и сам тег обязаны появиться вместе — иначе compose неделю ссылается на несуществующий образ. @@ -48,9 +48,9 @@ CHANGELOG `[Unreleased]/Security`, а разделы B.1–B.3 и C.4 вырез [development.md](development.md). 3. Перецелить ссылки из документации: [README.md](../README.md) («Open v1.x questions» — открытых вопросов там нет) → [progress.md](progress.md); - [code-review.md](code-review.md), [security.md](security.md), - [documentation-plan.md](documentation-plan.md), [progress.md](progress.md) и - шапку этого файла → на `progress.md`/`security.md`. + [security.md](security.md), [documentation-plan.md](documentation-plan.md), + [progress.md](progress.md) и шапку этого файла → на + `progress.md`/`security.md`. 4. В [progress.md](progress.md) убрать шаг «Открыть `implementation-plan.md`» — он выполнен. @@ -64,8 +64,23 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок `raw.githubusercontent.com` — это и есть единственная площадка проекта, зеркал больше нет.) +**Сводный индекс документации в README.** Ссылки на `docs/` разбросаны по +тексту README (блок в шапке плюс упоминания по месту), единого списка нет — +читателю, который ищет «а где вообще что», приходится вычитывать документ. +Стоит одного абзаца со списком всех файлов `docs/` и одной строкой на каждый. +Мелочь, но именно она делает набор документов набором, а не россыпью. + +**Опрос мониторинга у открытой, но незанятой вкладки.** Скрытая вкладка уже не +опрашивает сервер (фильтр на `htmx:beforeRequest` в +[panel.js](../internal/web/static/panel.js)). Остаток: вкладка на переднем +плане, с которой не работают, всё равно ходит раз в 5 с. Кандидат — адаптивный +интервал (5 с при активности, 30 с при простое) по `htmx:afterRequest` без +изменения `hx-trigger`. Ценность низкая: нагрузка — один SQL-запрос и рендер +фрагмента, так что это скорее гигиена, чем экономия. Допустимый исход — +осознанно не делать. + **Send-log vs `mail.log` (частично закрыто).** Persist позиции чтения сделан -(Фаза 3 [code-review.md](code-review.md), таблица `logtail_state`): после +(таблица `logtail_state`, миграция `0003`): после рестарта панели log-tailer дочитывает пропущенный хвост. Остаётся пересоздание контейнера — `mail.log` не в `/data` и теряется вместе с ним, такие строки навсегда останутся `queued`. Кандидаты, если станет больно: volume для лога, @@ -139,15 +154,37 @@ Windows → сборка и прогон на Debian-сервере, потом [development.md](development.md) и [progress.md](progress.md) — то есть на русском и вперемешку с внутренним состоянием проекта. -**Почему 2.x, а не v1.x** (перенесено из [code-review.md](code-review.md) § 10, -пункт R7, где стояло как «опционально v1.x»). Файл имеет смысл, когда есть -кому его читать: у проекта один разработчик и внешнего потока PR нет, поэтому -сейчас `CONTRIBUTING.md` был бы документом без аудитории и ещё одним местом, -где расходится правда о dev loop. Уместен вместе с тем, что реально открывает -проект вовне: английская документация процесса (сейчас RU/EN split — барьер -для EN-only контрибьюторов, [code-review.md](code-review.md) § 4) и первый -внешний интерес после публикации релиза. +**Почему 2.x, а не v1.x.** Файл имеет смысл, когда есть кому его читать: у +проекта один разработчик и внешнего потока PR нет, поэтому сейчас +`CONTRIBUTING.md` был бы документом без аудитории и ещё одним местом, где +расходится правда о dev loop. Уместен вместе с тем, что реально открывает +проект вовне: английская документация процесса (сейчас процессные документы — +`progress.md`, `roadmap.md`, `development.md` — на русском, а README и +`architecture.md` на английском; для EN-only контрибьютора это барьер) и +первый внешний интерес после публикации релиза. **Готово, когда:** `CONTRIBUTING.md` в корне описывает dev loop, требования к проверкам перед PR и протокол коммитов; [development.md](development.md) не дублирует его, а ссылается. + +--- + +## Разбиение `internal/web` на подпакеты — кандидат на 2.x + +**Что это.** `internal/web` — самый крупный пакет проекта: 47 файлов, ~3030 +строк, в одной плоскости лежат хендлеры всех разделов панели, сессии, +security-заголовки, проверка Origin, валидация форм и рендер шаблонов. +Кандидаты на выделение — `web/handlers` и `web/auth`, либо разрез по доменам +панели. + +**Почему 2.x, а не сейчас.** На нынешнем размере плоский пакет читается: имена +файлов (`handlers_domains.go`, `handlers_apps.go`, `handlers_monitor.go`) +работают не хуже каталогов, а разбиение потянуло бы за собой экспорт того, что +сейчас пакетно-приватно, — то есть расширение внутреннего API ради +косметики. Смысл появляется ровно тогда, когда пакет начнёт расти: обе задачи +2.x выше добавляют в него код — роль администратора домена приносит +авторизацию в каждый хендлер, входящий релей — отдельные страницы и хендлеры +входящих доменов. Рефакторинг дешевле делать перед этим ростом, чем после. + +**Готово, когда:** решение принято осознанно в момент старта 2.x — либо пакет +разрезан, либо зафиксировано, что он остаётся плоским.