docs: restructure development guide and move agent rules to .cursor
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Reorganize development.md (stack, deps, build, release, testing, CI); relocate agent rules to .cursor/rules; remove dev-host and mixfed.ru references from docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
description: Правила работы агента в репозитории SelfPost (ранее spec §12)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Правила для агентов
|
||||||
|
|
||||||
|
1. **Без git-коммитов** без явной инструкции в промпте.
|
||||||
|
2. После правок Go: `go build`, `go vet`; исправить все проблемы. `go test`,
|
||||||
|
когда тесты есть.
|
||||||
|
3. Перед «готово» для контейнерной задачи: образ собирается, контейнер стартует.
|
||||||
|
4. Итерации: минимальный скелет, затем фичи.
|
||||||
|
5. Требования безопасности — `docs/security.md`; внедрять с фичей, не откладывать.
|
||||||
|
6. Не делать out-of-scope (`docs/product.md`); не менять зафиксированные
|
||||||
|
допущения без согласования.
|
||||||
|
7. Крупные задачи — план до кода, если пользователь не утвердил план ранее.
|
||||||
|
8. Совместимость лицензий новых Go-зависимостей (permissive или GPL-family для
|
||||||
|
AGPL-3.0).
|
||||||
|
|
||||||
|
Маршрутизация моделей — `docs/progress.md` § «Модель по типу работы».
|
||||||
|
Предрелизная **ревизия** безопасности (не авторство) — Fable.
|
||||||
|
|
||||||
|
Протокол коммитов, CHANGELOG и закрытия фазы — `docs/progress.md` § «Коммиты»
|
||||||
|
и «Протокол закрытия фазы».
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Development-only material (secrets, server access notes) — never published.
|
# Development-only material (secrets, server access notes) — never published.
|
||||||
dev/
|
dev/
|
||||||
|
|
||||||
# Build artifacts (binaries are built on the dev server, not committed).
|
# Build artifacts (not committed).
|
||||||
/bin/
|
/bin/
|
||||||
*.exe
|
*.exe
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -39,6 +39,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
and outcome are the line under it, so what the message was and how it ended
|
and outcome are the line under it, so what the message was and how it ended
|
||||||
are both on the first line. The page takes the whole column rather than the
|
are both on the first line. The page takes the whole column rather than the
|
||||||
reading measure, as the other three monitoring pages already did.
|
reading measure, as the other three monitoring pages already did.
|
||||||
|
- `docs/development.md` restructured into stack, dependencies, build, release,
|
||||||
|
testing, and CI; agent rules moved to `.cursor/rules/agent-rules.mdc`;
|
||||||
|
dev-host-specific workflow and `mixfed.ru` references removed from docs.
|
||||||
|
|
||||||
## [0.6.0] - 2026-08-08
|
## [0.6.0] - 2026-08-08
|
||||||
|
|
||||||
@@ -617,5 +620,5 @@ of `docs/implementation-plan.md`).
|
|||||||
- Security pass against spec 7.6 (exec safety, config-write sanitization,
|
- Security pass against spec 7.6 (exec safety, config-write sanitization,
|
||||||
server-side validation, rate limiting, session/cookie hardening, output
|
server-side validation, rate limiting, session/cookie hardening, output
|
||||||
escaping, non-root panel) — full compliance, no code changes required.
|
escaping, non-root panel) — full compliance, no code changes required.
|
||||||
- Live production deployment on `selfpost.mixfed.ru` with a real Let's
|
- Live production deployment with a real Let's Encrypt certificate;
|
||||||
Encrypt certificate; end-to-end delivery confirmed (DKIM pass, SPF pass).
|
end-to-end delivery confirmed (DKIM pass, SPF pass).
|
||||||
|
|||||||
+155
-103
@@ -1,146 +1,198 @@
|
|||||||
# SelfPost — development
|
# SelfPost — разработка
|
||||||
|
|
||||||
**What this file is.** How to build, test, and ship changes. Current sprint
|
**Что это.** Как собрать, проверить и выкатить изменения. Текущий спринт —
|
||||||
state lives in [progress.md](progress.md) — read that first after `/clear`.
|
[progress.md](progress.md) (читать первым после `/clear`).
|
||||||
|
|
||||||
Product boundaries: [product.md](product.md). As-built layout:
|
Границы продукта: [product.md](product.md). As-built устройство:
|
||||||
[architecture.md](architecture.md).
|
[architecture.md](architecture.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Repository layout
|
## Технологический стек и инструменты
|
||||||
|
|
||||||
- `cmd/panel` — panel HTTP server + milter + log-tailer
|
| Компонент | Версия / заметки |
|
||||||
- `cmd/selfpost-backup` — CLI backup (`docker exec … selfpost-backup`)
|
|---|---|
|
||||||
- `internal/` — domain logic, store, web handlers, health checks
|
| **Go** | 1.26+ (`go.mod`); `CGO_ENABLED=0` — чистый Go, статическая линковка |
|
||||||
- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM wiring, entrypoint
|
| **SQLite** | `modernc.org/sqlite` (pure Go, без cgo) |
|
||||||
- `deploy/` — `docker-compose.yml`, proxy examples, `.env.example`
|
| **Сборка** | [Makefile](../Makefile): `vet`, `test`, `build`, `e2e` |
|
||||||
- `test/e2e/` — **separate Go module**; container integration tests
|
| **Контейнер** | Docker + Compose v2 на хосте разработки и в CI |
|
||||||
|
| **Образ (build stage)** | `golang:1.26-bookworm` — [build/Dockerfile](../build/Dockerfile) |
|
||||||
|
| **Образ (runtime)** | `debian:bookworm-slim` + Postfix, OpenDKIM, supervisord, SASL, logrotate |
|
||||||
|
| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) |
|
||||||
|
| **Реестр образов** | `ghcr.io/mixeme/selfpost` |
|
||||||
|
|
||||||
|
**Структура репозитория** (кратко; детали процессов — в [architecture.md](architecture.md)):
|
||||||
|
|
||||||
|
- `cmd/panel` — HTTP-панель + journal-milter + log-tailer
|
||||||
|
- `cmd/selfpost-backup` — CLI бэкапа (`docker exec … selfpost-backup`)
|
||||||
|
- `internal/` — доменная логика, store, web, health
|
||||||
|
- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM, entrypoint
|
||||||
|
- `deploy/` — `docker-compose.yml`, примеры прокси, `.env.example`
|
||||||
|
- `test/e2e/` — **отдельный Go-модуль**; контейнерные интеграционные тесты
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Local Go workflow
|
## Внешние библиотеки
|
||||||
|
|
||||||
Requires Go 1.26+ and `CGO_ENABLED=0` (pure Go SQLite).
|
Проект — **AGPL-3.0** ([LICENSE](../LICENSE)). Новые Go-зависимости — только
|
||||||
|
permissive или GPL-family (см. [.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)).
|
||||||
|
|
||||||
|
### Основной модуль (`go.mod`)
|
||||||
|
|
||||||
|
| Пакет | Версия | Репозиторий | Лицензия |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `github.com/emersion/go-milter` | v0.4.1 | <https://github.com/emersion/go-milter> | BSD-2-Clause |
|
||||||
|
| `golang.org/x/crypto` | v0.54.0 | <https://github.com/golang/crypto> | BSD-3-Clause |
|
||||||
|
| `modernc.org/sqlite` | v1.53.0 | <https://gitlab.com/cznic/sqlite> (зеркало: <https://github.com/modernc-org/sqlite>) | BSD-3-Clause |
|
||||||
|
|
||||||
|
Транзитивные зависимости — `go mod graph` / `go.sum`; все indirect в дереве
|
||||||
|
совместимы с AGPL-3.0.
|
||||||
|
|
||||||
|
### Модуль e2e (`test/e2e/go.mod`)
|
||||||
|
|
||||||
|
| Пакет | Версия | Репозиторий | Лицензия |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `github.com/emersion/go-msgauth` | v0.6.8 | <https://github.com/emersion/go-msgauth> | BSD-2-Clause |
|
||||||
|
|
||||||
|
Тестовый модуль не входит в граф основного `go build` и не попадает в образ.
|
||||||
|
|
||||||
|
### Пакеты Debian в runtime-образе
|
||||||
|
|
||||||
|
Postfix, OpenDKIM, `supervisord`, `sasl2-bin`, `logrotate` и др. —
|
||||||
|
из репозиториев Debian bookworm; лицензии — в `copyright` соответствующих
|
||||||
|
пакетов на <https://packages.debian.org/bookworm/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сборка исполняемого файла и образа
|
||||||
|
|
||||||
|
### Локальные бинарники
|
||||||
|
|
||||||
|
Требуется Go 1.26+ и `CGO_ENABLED=0`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build # bin/panel, bin/selfpost-backup (VERSION=dev по умолчанию)
|
||||||
|
make build VERSION=1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Или напрямую:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel
|
||||||
|
```
|
||||||
|
|
||||||
|
Версия вшивается в оба бинарники через `-ldflags` и **должна совпадать с тегом
|
||||||
|
Docker-образа** — restore проверяет совместимость версий бэкапа.
|
||||||
|
|
||||||
|
### Docker-образ
|
||||||
|
|
||||||
|
Из корня репозитория:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
В Dockerfile: build stage (`go vet`, `go build` с `VERSION`), runtime stage
|
||||||
|
(Debian + почтовый стек). См. [architecture.md](architecture.md) § Image and
|
||||||
|
processes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сборка релиза
|
||||||
|
|
||||||
|
Релизный образ публикуется **только по тегу** `vX.Y.Z` (не на каждый push в
|
||||||
|
`main`). Тег — единственный источник версии: из него берутся тег образа и
|
||||||
|
`-ldflags` в бинарниках, чтобы они не расходились.
|
||||||
|
|
||||||
|
**Шаги (по явному запросу):**
|
||||||
|
|
||||||
|
1. Закрыть `[Unreleased]` в [CHANGELOG.md](../CHANGELOG.md).
|
||||||
|
2. Создать и запушить git-тег `vX.Y.Z`.
|
||||||
|
3. Workflow [release.yml](../.github/workflows/release.yml) собирает, гейтит
|
||||||
|
e2e и публикует `ghcr.io/mixeme/selfpost:X.Y.Z`.
|
||||||
|
4. Обновить закреплённый тег в [deploy/docker-compose.yml](../deploy/docker-compose.yml)
|
||||||
|
(см. [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»).
|
||||||
|
|
||||||
|
Обычные коммиты **не** публикуют образ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Статический анализ и unit-тесты
|
||||||
|
|
||||||
|
Основной модуль (`go test ./...`); e2e — отдельный модуль, см. ниже.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make vet # go vet ./...
|
make vet # go vet ./...
|
||||||
make test # go test ./...
|
make test # go test ./...
|
||||||
make build # bin/panel, bin/selfpost-backup (VERSION=dev by default)
|
|
||||||
make build VERSION=1.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or directly:
|
Или напрямую:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
gofmt -l . # в CI — fail при расхождении
|
||||||
go vet ./...
|
go vet ./...
|
||||||
go test ./...
|
go test ./...
|
||||||
go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Env documentation regression:** `go test ./cmd/panel -run TestLoadConfig` —
|
### Регресс документации env
|
||||||
new `loadConfig` keys must appear in guide.md env lists
|
|
||||||
|
`go test ./cmd/panel -run TestLoadConfig` — каждый новый ключ `loadConfig`
|
||||||
|
должен появиться в списках env в [guide.md](guide.md)
|
||||||
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
||||||
|
|
||||||
---
|
### End-to-end (контейнерный suite)
|
||||||
|
|
||||||
## End-to-end tests
|
Отдельный Go-модуль `test/e2e/`; **не** входит в `go test ./...` основного
|
||||||
|
модуля.
|
||||||
Hermetic container suite (implemented as `test/e2e/`, separate Go module):
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make e2e
|
make e2e
|
||||||
# equivalent: cd test/e2e && go test -v -timeout 20m ./...
|
# то же: cd test/e2e && go test -v -timeout 20m ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Stack:** shipped [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
**Стек:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
||||||
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same
|
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — те же
|
||||||
`cap_drop`/`cap_add`/`no-new-privileges` as production. Override uses high
|
`cap_drop`/`cap_add`/`no-new-privileges`, что в production. Override: высокие
|
||||||
ports (`20465`/`20587`/`20080`), test hostname, self-signed TLS,
|
порты (`20465`/`20587`/`20080`), тестовый hostname, self-signed TLS,
|
||||||
`PANEL_COOKIE_SECURE=false`, isolated compose project. Mail is hermetic: CoreDNS
|
`PANEL_COOKIE_SECURE=false`, изолированный compose-проект. Почта герметична:
|
||||||
fake zone + Postfix `smtp-sink` as sink-MX; DKIM TXT is scraped from the panel
|
CoreDNS (fake zone) + Postfix `smtp-sink` как sink-MX; DKIM TXT скрапится с
|
||||||
and published into the zone so the test verifies the record the operator would
|
панели и публикуется в зону — тест проверяет записи, которые оператор реально
|
||||||
use.
|
использует.
|
||||||
|
|
||||||
**Coverage (summary):** full bootstrap → SMTP AUTH → delivery → DKIM verify →
|
**Покрытие (сводка):** bootstrap → SMTP AUTH → доставка → DKIM verify →
|
||||||
send-log `queued → sent`; negatives (no AUTH, relay, sender/login mismatch, L1/L2
|
send-log `queued → sent`; негативы (без AUTH, relay, sender/login mismatch,
|
||||||
limits, milter fail-open, bad `SELFPOST_HOSTNAME`, session survives
|
L1/L2 лимиты, fail-open milter, неверный `SELFPOST_HOSTNAME`, сессия после
|
||||||
`docker restart`). Polling with timeouts only — no fixed `sleep`.
|
`docker restart`). Только polling с таймаутами — без фиксированных `sleep`.
|
||||||
|
|
||||||
Requires **Docker + Compose v2** on the test host. Not included in `go test ./...`
|
Требует **Docker + Compose v2** на машине, где гоняется suite.
|
||||||
of the main module.
|
|
||||||
|
|
||||||
**CI** ([release.yml](../.github/workflows/release.yml)): tag `vX.Y.Z` triggers
|
|
||||||
`prepare` (version from tag) → native matrix `[ubuntu-latest, ubuntu-24.04-arm]`
|
|
||||||
— build `--load`, e2e, push per-arch tag → `merge` publishes unified
|
|
||||||
`ghcr.io/...:X.Y.Z` via `docker buildx imagetools create`. Failed e2e blocks the
|
|
||||||
image. Ordinary pushes still run only `vet`/`test` in [test.yml](../.github/workflows/test.yml).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dev server (full container)
|
## CI
|
||||||
|
|
||||||
When unit tests are not enough — Postfix, OpenDKIM, supervisord, real SMTP:
|
Workflows в [.github/workflows/](../.github/workflows/). Что именно гоняется —
|
||||||
|
в [§ Тестирование](#тестирование) выше.
|
||||||
|
|
||||||
**Typical setup:** edit locally → sync tree to dev host → build/test there.
|
### `test.yml` — каждый push и PR в `main`
|
||||||
|
|
||||||
Documented dev host: `selfpost.mixfed.ru` (Debian 12). Sync example from
|
`gofmt -l` → `go vet ./...` → `go test ./...` (основной модуль, без e2e).
|
||||||
[progress.md](progress.md):
|
|
||||||
|
|
||||||
```sh
|
### `release.yml` — push тега `vX.Y.Z` или `workflow_dispatch`
|
||||||
tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru \
|
|
||||||
'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'
|
```
|
||||||
|
prepare (версия из тега)
|
||||||
|
→ build [matrix: ubuntu-latest / ubuntu-24.04-arm]
|
||||||
|
→ docker build --load (VERSION из тега)
|
||||||
|
→ e2e (test/e2e)
|
||||||
|
→ push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64
|
||||||
|
→ merge
|
||||||
|
→ docker buildx imagetools create → единый манифест X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
On the server (Go in `/usr/local/go/bin` if not in PATH):
|
Нативная матрица per-arch (без QEMU): полный стек Postfix/OpenDKIM под
|
||||||
|
эмуляцией для e2e непрактичен. Сначала e2e, затем push — в registry попадают
|
||||||
|
байты, прошедшие гейт.
|
||||||
|
|
||||||
```sh
|
Провал e2e **блокирует** публикацию образа.
|
||||||
cd /root/selfpost-src
|
|
||||||
/usr/local/go/bin/go vet ./...
|
|
||||||
/usr/local/go/bin/go test ./...
|
|
||||||
docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
|
|
||||||
```
|
|
||||||
|
|
||||||
Manual smoke on production-like host: panel at `https://selfpost.mixfed.ru`,
|
|
||||||
real LE cert, live deliverability — not replaceable by e2e alone (no outbound
|
|
||||||
25 on CI runners, no real PTR/reputation).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commit and changelog protocol
|
|
||||||
|
|
||||||
From [progress.md](progress.md):
|
|
||||||
|
|
||||||
1. Meaningful step → entry under `[Unreleased]` in [CHANGELOG.md](../CHANGELOG.md)
|
|
||||||
(Keep a Changelog format).
|
|
||||||
2. Commit on `main` unless asked for a branch. **Do not commit unless the user
|
|
||||||
asks.**
|
|
||||||
3. Before `/clear` at end of a phase: update `progress.md`, verify acceptance
|
|
||||||
criteria, CHANGELOG, final commit.
|
|
||||||
|
|
||||||
Documentation changes that add/rename env vars, panel routes, or observable
|
|
||||||
mail behaviour ship in the **same commit** as the code change.
|
|
||||||
|
|
||||||
Release tagging and image push — only on explicit request.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent rules (formerly spec §12)
|
|
||||||
|
|
||||||
1. **No git commits** without explicit instruction in the prompt.
|
|
||||||
2. After Go changes: `go build`, `go vet`; fix all issues. Run `go test` when
|
|
||||||
tests exist.
|
|
||||||
3. Before calling a container task done: image builds and container starts.
|
|
||||||
4. Iterate: minimal skeleton first, then features.
|
|
||||||
5. Security requirements in [security.md](security.md) — implement with the feature,
|
|
||||||
not deferred.
|
|
||||||
6. Do not implement out-of-scope items ([product.md](product.md)) or change
|
|
||||||
fixed assumptions without agreement.
|
|
||||||
7. For large tasks: propose a plan before coding unless the user already approved
|
|
||||||
one.
|
|
||||||
8. Check licence compatibility of new Go dependencies (permissive or
|
|
||||||
GPL-family for AGPL-3.0 project).
|
|
||||||
|
|
||||||
Model routing (from progress): security/infra → Opus; UI/docs → Sonnet;
|
|
||||||
trivial mechanics → Haiku. Pre-release security **review** (not authorship) →
|
|
||||||
Fable.
|
|
||||||
|
|||||||
+12
-9
@@ -43,21 +43,24 @@
|
|||||||
|
|
||||||
- **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется.
|
- **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется.
|
||||||
- **B.1 реализован** (не выкачен на прод): сессии переехали в SQLite (`internal/store/migrations/0002_sessions.sql`, `internal/store/sessions.go`, `internal/web/session.go`) — хранится SHA-256 токена, не сам токен; скользящий срок бездействия `PANEL_SESSION_IDLE_DAYS` (по умолчанию 7 дней, без абсолютного потолка); запись в БД продлевается не чаще раза в час (`renewThreshold`); опросы мониторинга (`GET` с `HX-Request`) продление не триггерят (`isSessionActivity` в `internal/web/middleware.go`); `Max-Age` cookie выставляется тем же значением при логине и при продлении (`setSessionCookie`); смена пароля разлогинивает все сессии кроме текущей (уже было, теперь через БД). Проверено на стенде: логин → рестарт процесса панели → сессия жива по старой cookie; HX-Request-опрос и повторный GET внутри часового окна не шлют `Set-Cookie`. `go vet`/`go test ./...`/`gofmt -l .` чистые.
|
- **B.1 реализован** (не выкачен на прод): сессии переехали в SQLite (`internal/store/migrations/0002_sessions.sql`, `internal/store/sessions.go`, `internal/web/session.go`) — хранится SHA-256 токена, не сам токен; скользящий срок бездействия `PANEL_SESSION_IDLE_DAYS` (по умолчанию 7 дней, без абсолютного потолка); запись в БД продлевается не чаще раза в час (`renewThreshold`); опросы мониторинга (`GET` с `HX-Request`) продление не триггерят (`isSessionActivity` в `internal/web/middleware.go`); `Max-Age` cookie выставляется тем же значением при логине и при продлении (`setSessionCookie`); смена пароля разлогинивает все сессии кроме текущей (уже было, теперь через БД). Проверено на стенде: логин → рестарт процесса панели → сессия жива по старой cookie; HX-Request-опрос и повторный GET внутри часового окна не шлют `Set-Cookie`. `go vet`/`go test ./...`/`gofmt -l .` чистые.
|
||||||
- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (`selfpost.mixfed.ru`, отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на dev-сервере; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится).
|
- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на стенде; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится).
|
||||||
- **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (`selfpost.mixfed.ru`, отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые.
|
- **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: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` пути.
|
- **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` — локальный прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на стенде**: `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` закрыты).
|
- **Документация:** план D1–D9 закрыт ([documentation-plan.md](documentation-plan.md) — только метод и правила поддержки). Хвост v1.x — [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»; из него остался только бамп тега образа (Quick start и `docs/logo` закрыты).
|
||||||
- **Рецензирование кодовой базы** (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-истории.
|
- **Рецензирование кодовой базы** (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 зелено.
|
- **§ 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)** (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 плана ревизии выполнена (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 ./...` чистые в обоих модулях.
|
||||||
- **Фаза 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-сервере.
|
- **Фаза 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) — кандидат при следующем прогоне на стенде.
|
||||||
- **Фаза 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-сервере.
|
- **Фаза 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 локально) — кандидат на следующий прогон на стенде.
|
||||||
- **Фаза 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-сервере.
|
- **Фаза 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 локально) — кандидат на следующий прогон на стенде.
|
||||||
- **Добор по плану ревизии выполнен (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 локально).
|
- **Добор по плану ревизии выполнен (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) закрыты.
|
- **Дальше:** релизный гейт (Фаза 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+, роль администратора домена).
|
- **Принятые риски** — [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/` это делает).
|
- **Прод:** инстанс с реальным Let's Encrypt сертификатом и живым deliverability (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает).
|
||||||
|
|
||||||
## Рабочая петля (dev loop) — ВАЖНО
|
## Рабочая петля (dev loop) — ВАЖНО
|
||||||
|
|
||||||
Локально (Windows, `D:\Local\Git\selfpost`) **нет Go и Docker** — только редактирование и git. Вся сборка/тесты идут на dev-сервере `selfpost.mixfed.ru` (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → залить дерево на сервер → `go build`/`go vet`/`docker build`/тесты там. `rsync` в локальном git-bash нет, поэтому дерево едет tar'ом по ssh: `tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru 'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'`; Go на сервере — в `/usr/local/go/bin` (не в `PATH` по умолчанию); образ — `docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .`. Источник истины и git-история — локальный репозиторий; сервер — только исполнитель сборки/тестов. Подключение: `ssh root@selfpost.mixfed.ru` (по ключу).
|
Сборка, unit-тесты и e2e требуют Go 1.26+ и Docker + Compose v2 — см.
|
||||||
|
[development.md](development.md). Конкретная связка машин (всё локально,
|
||||||
|
отдельный сервер, только CI) у каждого разработчика своя; источник истины —
|
||||||
|
git-репозиторий.
|
||||||
|
|||||||
+6
-6
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
**Статус:** здесь собран объём, отнесённый к релизной линии **2.x.x** — вне
|
**Статус:** здесь собран объём, отнесённый к релизной линии **2.x.x** — вне
|
||||||
базового объёма v1.0/v1.x (v1.x — только исходящий релей). Реализация —
|
базового объёма v1.0/v1.x (v1.x — только исходящий релей). Реализация —
|
||||||
только после явного согласования ([product.md](product.md), [development.md](development.md) § Agent rules):
|
только после явного согласования ([product.md](product.md),
|
||||||
|
[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)):
|
||||||
[product.md](product.md) явно исключает часть этого объёма (приём входящей
|
[product.md](product.md) явно исключает часть этого объёма (приём входящей
|
||||||
почты; несколько пользователей/роли), поэтому включение — сознательное
|
почты; несколько пользователей/роли), поэтому включение — сознательное
|
||||||
расширение границ проекта, а не доработка по своей инициативе. Присутствие
|
расширение границ проекта, а не доработка по своей инициативе. Присутствие
|
||||||
@@ -128,7 +129,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
|||||||
|
|
||||||
**Риски:** open relay/backscatter (снимается `relay_domains` + `relay_recipient_maps` + `reject_unauth_destination`); потеря origin IP для фильтрации на backend'е при пересылке (снимается milter-хуком антиспама + нативным DNSBL на входном хопе, где origin IP ещё виден); порт 25 на приём расширяет поверхность атаки (по умолчанию выключено). **Модель:** Opus (инфра/безопасность, риск open relay). **Внешняя зависимость деплоя:** опциональный antispam-контейнер — вне образа SelfPost, поднимается оператором при включении опции.
|
**Риски:** open relay/backscatter (снимается `relay_domains` + `relay_recipient_maps` + `reject_unauth_destination`); потеря origin IP для фильтрации на backend'е при пересылке (снимается milter-хуком антиспама + нативным DNSBL на входном хопе, где origin IP ещё виден); порт 25 на приём расширяет поверхность атаки (по умолчанию выключено). **Модель:** Opus (инфра/безопасность, риск open relay). **Внешняя зависимость деплоя:** опциональный antispam-контейнер — вне образа SelfPost, поднимается оператором при включении опции.
|
||||||
|
|
||||||
**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([development.md](development.md)) до кодирования.
|
**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)) до кодирования.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
|||||||
**Почему 2.x, а не v1.x.** [product.md](product.md) относит «несколько пользователей
|
**Почему 2.x, а не v1.x.** [product.md](product.md) относит «несколько пользователей
|
||||||
панели, роли» к out of scope (один администратор), поэтому появление второго
|
панели, роли» к out of scope (один администратор), поэтому появление второго
|
||||||
субъекта — расширение границ проекта, как и Фаза O1: сначала согласование
|
субъекта — расширение границ проекта, как и Фаза O1: сначала согласование
|
||||||
([development.md](development.md)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена.
|
([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена.
|
||||||
|
|
||||||
*(Прежняя формулировка этого пункта — «2FA и несколько администраторов» — заменена: 2FA снята с рассмотрения, а «несколько администраторов» уточнено до одной конкретной роли, потому что нужна не вторая копия всевластного админа, а ограниченный доступ владельца отдельного домена.)*
|
*(Прежняя формулировка этого пункта — «2FA и несколько администраторов» — заменена: 2FA снята с рассмотрения, а «несколько администраторов» уточнено до одной конкретной роли, потому что нужна не вторая копия всевластного админа, а ограниченный доступ владельца отдельного домена.)*
|
||||||
|
|
||||||
@@ -147,9 +148,8 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок
|
|||||||
|
|
||||||
## `CONTRIBUTING.md` — кандидат на 2.x
|
## `CONTRIBUTING.md` — кандидат на 2.x
|
||||||
|
|
||||||
**Что это.** Точка входа для стороннего контрибьютора: dev loop (правка на
|
**Что это.** Точка входа для стороннего контрибьютора: dev loop, маршрутизация
|
||||||
Windows → сборка и прогон на Debian-сервере, потому что Docker локально нет),
|
моделей по типу работы, протокол коммитов, требование
|
||||||
маршрутизация моделей по типу работы, протокол коммитов, требование
|
|
||||||
`gofmt`/`vet`/`test`/`make e2e` до PR. Сейчас всё это есть, но в
|
`gofmt`/`vet`/`test`/`make e2e` до PR. Сейчас всё это есть, но в
|
||||||
[development.md](development.md) и [progress.md](progress.md) — то есть на
|
[development.md](development.md) и [progress.md](progress.md) — то есть на
|
||||||
русском и вперемешку с внутренним состоянием проекта.
|
русском и вперемешку с внутренним состоянием проекта.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// smtpsAddr is where compose.override.yml publishes the primary implicit-TLS
|
// smtpsAddr is where compose.override.yml publishes the primary implicit-TLS
|
||||||
// submission port (465 in the shipped compose, remapped here to stay off the
|
// submission port (465 in the shipped compose, remapped here to avoid clashing
|
||||||
// dev server's real ports).
|
// with a local deployment on the same host).
|
||||||
const smtpsAddr = "127.0.0.1:20465"
|
const smtpsAddr = "127.0.0.1:20465"
|
||||||
|
|
||||||
// sendAttempt is one SMTP transaction against the stand. An empty authLogin
|
// sendAttempt is one SMTP transaction against the stand. An empty authLogin
|
||||||
|
|||||||
Reference in New Issue
Block a user