docs: remove code-review.md, carry the open items into roadmap
The review's plan is finished — phase 0 (bar the two release-commit steps),
1, 1.5, 2 and 3 are all closed — and what remained in the document was a second
copy of things that already live in architecture.md, security.md, roadmap.md or
the code comments: the GUI compromise table is in panel.css/panel.js/
middleware.go/handlers_auth.go, the single SQLite connection and the dual
cookie names are explained where they are implemented, the accepted gaps are in
security.md, and the model-routing table names progress.md and development.md
as its own source. A second copy of a fact is a place for it to go stale.
Four items were genuinely open and had no other home, so they moved to
roadmap.md rather than disappearing:
- splitting internal/web into subpackages (2.x) — with the reason to wait: the
flat package still reads at 47 files, and both 2.x features grow it, so the
cut is worth making before that growth, not now;
- a consolidated documentation index in the README (v1.x tail);
- the adaptive polling interval for a tab that is visible but idle — the hidden
case is already handled, and the remainder is explicitly allowed to end as
"decided not to";
- CONTRIBUTING.md, already moved to 2.x in the previous commit.
References retargeted: progress.md (7), roadmap.md (5), implementation-plan.md
(1). The CHANGELOG entries that cite the document are left as written — they
describe what happened at the time. The review text stays in git history at
aaf0711.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 `<span>` | `panel.css` | `max-width` on `<td>` 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
+7
-8
@@ -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.example.com`, отдельный образ `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.example.com`)**: `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.example.com`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||
|
||||
|
||||
+52
-15
@@ -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 — либо пакет
|
||||
разрезан, либо зафиксировано, что он остаётся плоским.
|
||||
|
||||
Reference in New Issue
Block a user