docs: align operator and as-built docs with the code
Fix setup URL shape, import encryption UI, architecture layering/routes, and stale plan/roadmap pointers so the prose matches what the tree does. Co-Authored-By: Composer <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,6 +51,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
|
||||
### Changed
|
||||
|
||||
- Docs aligned with the code: setup URL is `/setup/<token>` (README and
|
||||
guide; local trial rewrites the printed `https://<hostname>/…` link to
|
||||
`http://127.0.0.1:8080/…`); domain import uses the file extension / magic
|
||||
bytes for the password field, not an "encrypted" checkbox; architecture
|
||||
layering and route table match `web`→`store` and `POST /domains/import`;
|
||||
OpenDKIM drops to `opendkim` via `UserID`; guide drops the archived
|
||||
"spec 7.5" pointer, clarifies `POSTFIX_SENDER_LOGIN_MAPS` vs panel writes,
|
||||
and states logrotate keeps 14 daily files. Intermediate CHANGELOG cuts vs
|
||||
the still-pinned compose `0.1.0` image are called out in the guide and
|
||||
`development.md`. Roadmap / implementation-plan point at CHANGELOG
|
||||
`[0.5.0]` Security and refreshed `internal/web` size / symbol links.
|
||||
- Full backups no longer carry `/data/log`. It is Postfix's raw log plus its
|
||||
fourteen rotated copies — diagnostic output rather than state to restore, and
|
||||
otherwise by far the largest thing in the archive.
|
||||
|
||||
@@ -105,7 +105,10 @@ docker logs selfpost-try 2>&1 | grep -m1 'http'
|
||||
docker exec selfpost-try cat /data/setup-token
|
||||
```
|
||||
|
||||
Open the printed `http://…/setup?token=…` link before it expires.
|
||||
The printed URL is `https://mail.local.test/setup/<token>`. For this local
|
||||
trial rewrite it to `http://127.0.0.1:8080/setup/<token>` (same path token;
|
||||
`PANEL_COOKIE_SECURE=false` so the cookie works over plain HTTP). Open it
|
||||
before it expires.
|
||||
|
||||
When finished:
|
||||
|
||||
|
||||
+14
-8
@@ -26,7 +26,7 @@ Managed programs ([build/supervisord.conf](../build/supervisord.conf)):
|
||||
|
||||
| Program | User | Priority | Role |
|
||||
|---|---|---|---|
|
||||
| `opendkim` | opendkim | 100 | DKIM signing milter |
|
||||
| `opendkim` | root → `opendkim` (`UserID` in opendkim.conf) | 100 | DKIM signing milter |
|
||||
| `panel` | panel | 200 | HTTP UI + journal-milter + log-tailer goroutine |
|
||||
| `postfix` | root (wrapper) | 300 | MTA — started only after both milter sockets exist |
|
||||
| `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) |
|
||||
@@ -151,8 +151,10 @@ state for an older message and the page reports it as such, not as a failure.
|
||||
|
||||
## Panel HTTP surface
|
||||
|
||||
Route table: [internal/web/web.go](../internal/web/web.go). Authenticated
|
||||
unless noted.
|
||||
Canonical routes: [internal/web/web.go](../internal/web/web.go). Authenticated
|
||||
unless noted. The table below is a summary — HTMX fragment endpoints
|
||||
(`/status/fragment`, `/deliveries/rows`, `/mail-queue/body`,
|
||||
`/system-log/body`, …) and every POST variant live in `web.go`.
|
||||
|
||||
| Route | Purpose |
|
||||
|---|---|
|
||||
@@ -161,12 +163,13 @@ unless noted.
|
||||
| `/login`, `/logout` | Session auth |
|
||||
| `/status` | Process, cert, socket, PTR checks; machine CPU/memory/network |
|
||||
| `/domains`, `/domains/*` | Domain and application CRUD, DKIM, L2 limits |
|
||||
| `/domains/import` | Domain import (`POST`; form on the Backup page) |
|
||||
| `/deliveries` | Send log with filters |
|
||||
| `/deliveries/{id}` | One send-log row in full, with its `mail.log` lines |
|
||||
| `/mail-queue` | Postfix queue view |
|
||||
| `/system-log` | `mail.log` tail |
|
||||
| `/reload` | Reload OpenDKIM + Postfix maps |
|
||||
| `/backup` | Full backup download, domain import |
|
||||
| `/backup` | Full backup download (page also hosts the import form) |
|
||||
| `/account` | Admin username/password |
|
||||
|
||||
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
||||
@@ -198,10 +201,12 @@ cookie and idle timeout has not expired.
|
||||
|
||||
## Code layers
|
||||
|
||||
Handlers never touch SQLite or the filesystem directly; every write that has to
|
||||
land in more than one place (SQLite row, `sasldb2` entry, Postfix map, OpenDKIM
|
||||
table) goes through a service, which is also where the rollback of a partial
|
||||
failure lives. The adapters below the services are the only code that knows
|
||||
Multi-store writes that must land in more than one place (SQLite row,
|
||||
`sasldb2` entry, Postfix map, OpenDKIM table) go through a service, which is
|
||||
also where the rollback of a partial failure lives. Handlers may call
|
||||
`store` directly for single-table reads and simple writes (sessions, admin,
|
||||
send-log queries); the first-run setup-token file is read and written in
|
||||
`web` itself. The adapters below the services are the only code that knows
|
||||
about Postfix, OpenDKIM, DNS or the log file, which is what makes them
|
||||
substitutable in tests — `milter.Store`, `app.SenderMaps` and
|
||||
`logtail.StatusStore` are the seams the unit tests replace with fakes.
|
||||
@@ -236,6 +241,7 @@ flowchart TB
|
||||
panel --> logtail
|
||||
backupcli --> backupPkg
|
||||
backupcli --> secretfile
|
||||
web --> store
|
||||
web --> domainSvc
|
||||
web --> appSvc
|
||||
web --> backupPkg
|
||||
|
||||
+8
-4
@@ -44,7 +44,7 @@ mechanics → Haiku. Reviewers must not be the author of the code under review.
|
||||
|---|---|
|
||||
| **Go** | 1.26+ (`go.mod`); `CGO_ENABLED=0` — pure Go, static linking |
|
||||
| **SQLite** | `modernc.org/sqlite` (pure Go, no cgo) |
|
||||
| **Build** | [Makefile](../Makefile): `vet`, `test`, `build`, `e2e` |
|
||||
| **Build** | [Makefile](../Makefile): primary targets `vet`, `test`, `build`, `e2e` (also `all`, `clean`) |
|
||||
| **Container** | Docker + Compose v2 on the dev host and in CI |
|
||||
| **Image (build stage)** | `golang:1.26-bookworm` — [build/Dockerfile](../build/Dockerfile) |
|
||||
| **Image (runtime)** | `debian:bookworm-slim` + Postfix, OpenDKIM, supervisord, SASL, logrotate |
|
||||
@@ -163,7 +163,11 @@ The release image is published **only on tag** `vX.Y.Z` (not on every push to
|
||||
commit as the tag (see [roadmap.md](roadmap.md) § «v1.x — documentation and
|
||||
deploy tail»).
|
||||
|
||||
Ordinary commits **do not** publish an image.
|
||||
Ordinary commits **do not** publish an image. Intermediate CHANGELOG version
|
||||
cuts (`0.2.0`…`0.6.0`) document history on `main`; the compose pin and the only
|
||||
git release tag may lag until an explicit image publish (today: compose
|
||||
`0.1.0`, tag `v0.0.1` — see [roadmap.md](roadmap.md) § «v1.x — documentation
|
||||
and deploy tail»).
|
||||
|
||||
---
|
||||
|
||||
@@ -200,8 +204,8 @@ go test ./...
|
||||
|
||||
### Env documentation regression
|
||||
|
||||
`go test ./cmd/panel -run TestLoadConfig` — every new `loadConfig` key must
|
||||
appear in the env lists in [guide.md](guide.md)
|
||||
`go test ./cmd/panel -run 'TestLoadConfigKeysDocumented|TestBuildScriptKeysDocumented|TestDocumentedKeysAreRead'`
|
||||
— every new `loadConfig` key must appear in the env lists in [guide.md](guide.md)
|
||||
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
||||
|
||||
### End-to-end (container suite)
|
||||
|
||||
+33
-18
@@ -59,6 +59,12 @@ reverse proxy, no `./certs` bind mount — Postfix still starts, but the Status
|
||||
page will report missing TLS material until you mount PEM files at
|
||||
`/etc/postfix/tls/fullchain.pem` and `privkey.pem`.
|
||||
|
||||
The one-time setup link is always printed as
|
||||
`https://<SELFPOST_HOSTNAME>/setup/<token>` (and written the same way to
|
||||
`/data/setup-token`). For a local trial that means rewriting the host and
|
||||
scheme to `http://127.0.0.1:8080/setup/<token>` — the path token is what
|
||||
matters; the hostname in the printed URL is not reachable as written.
|
||||
|
||||
**What works:** the full panel — setup, domains, applications, deliveries view,
|
||||
mail queue, system log. **What does not:** reliable outbound delivery to the
|
||||
public internet (no PTR, no real DNS for your domains, port 25 may be blocked
|
||||
@@ -116,15 +122,17 @@ supported configuration:
|
||||
`DKIM_SELECTOR_DEFAULT` (`selfpost`), `SASL_DB_PATH`
|
||||
(`/data/sasl/sasldb2`), `SASL_REALM` (defaults to `SELFPOST_HOSTNAME`),
|
||||
`POSTFIX_DIR` (`/data/postfix`), `POSTFIX_SENDER_LOGIN_MAPS`
|
||||
(`/data/postfix/sender_login_maps`).
|
||||
(`/data/postfix/sender_login_maps` — read by Postfix config only; the panel
|
||||
always writes `<POSTFIX_DIR>/sender_login_maps`, so overriding this env alone
|
||||
desyncs the map Postfix reads from the file the panel maintains).
|
||||
- **Milter and Postfix startup:** `MILTER_CONNECT_TIMEOUT` (`15s`),
|
||||
`MILTER_COMMAND_TIMEOUT` (`15s`), `MILTER_CONTENT_TIMEOUT` (`30s`),
|
||||
`MILTER_WAIT_TIMEOUT` (`30` seconds).
|
||||
- **Background maintenance:** `TLS_RELOAD_INTERVAL_SECONDS` (`86400` — daily
|
||||
`postfix reload` to pick up renewed certificates),
|
||||
`LOGROTATE_INTERVAL_SECONDS` (`21600` — check `mail.log` rotation every six
|
||||
hours; rotated logs are kept 14 days and each rotation triggers
|
||||
`postfix reload`).
|
||||
hours; logrotate keeps 14 rotated files on a daily schedule, and each
|
||||
rotation triggers `postfix reload`).
|
||||
|
||||
## DNS setup
|
||||
|
||||
@@ -212,8 +220,9 @@ service healthy and will mail be accepted?"
|
||||
hours. It lives in the data volume, so it survives a container recreate along
|
||||
with the rest of the state — `./data/log/` on the host — but it is *not*
|
||||
included in backups: it is diagnostics, not state.
|
||||
- **Backup** (`/backup`) — download a full-server backup or import a
|
||||
single-domain export. See [Backup, restore, and moving a single domain](#backup-restore-and-moving-a-single-domain).
|
||||
- **Backup** (`/backup`) — download a full-server backup; the same page hosts
|
||||
the domain-import form (`POST /domains/import`). See
|
||||
[Backup, restore, and moving a single domain](#backup-restore-and-moving-a-single-domain).
|
||||
- **Account** (`/account`) — change the administrator username and/or password.
|
||||
Application SASL logins are separate and are not changed here.
|
||||
|
||||
@@ -243,9 +252,10 @@ or poll `docker inspect` health state on the host.
|
||||
**First-time setup link.** On first start the one-time setup URL is printed in
|
||||
the container log (`docker compose logs -f`) and written to `/data/setup-token`
|
||||
inside the container — `./data/setup-token` on the host, mode `0600` — then
|
||||
deleted when setup completes. The link is a bearer token valid for ten minutes.
|
||||
If this host ships container logs to a central aggregator, prefer reading the
|
||||
file:
|
||||
deleted when setup completes. The link is
|
||||
`https://<SELFPOST_HOSTNAME>/setup/<token>` (path token, not a query string),
|
||||
valid for ten minutes. If this host ships container logs to a central
|
||||
aggregator, prefer reading the file:
|
||||
|
||||
```sh
|
||||
docker compose exec selfpost cat /data/setup-token
|
||||
@@ -273,10 +283,11 @@ exceeded, Postfix returns a 4xx and the refusal is recorded in Deliveries as
|
||||
|
||||
## Backup, restore, and moving a single domain
|
||||
|
||||
Two related but distinct operations — spec 7.5:
|
||||
Two related but distinct operations
|
||||
([architecture.md](architecture.md) § Persistence):
|
||||
|
||||
- **Full backup** (whole `/data`: SQLite, all domains' DKIM keys, all
|
||||
applications' SASL credentials, `manifest.json` with the version that
|
||||
- **Full backup** (whole `/data` except `log/`: SQLite, all domains' DKIM keys,
|
||||
all applications' SASL credentials, `manifest.json` with the version that
|
||||
created it): panel button (*Backup* → *Full backup*), or from the
|
||||
host:
|
||||
```sh
|
||||
@@ -329,8 +340,10 @@ with AES-256-GCM, in chunks, so a truncated or altered file fails to open rather
|
||||
than restoring quietly. **SelfPost does not store the password** — lose it and
|
||||
the file is unrecoverable, which is the entire point.
|
||||
|
||||
*Import a domain* takes an encrypted export directly: tick **The file is
|
||||
encrypted** and give the password.
|
||||
*Import a domain* takes an encrypted export directly: choose a `.spde` file and
|
||||
the password field appears (driven by the file extension in the browser; the
|
||||
server also detects the envelope by its magic bytes). A plain `.json` export
|
||||
needs no password.
|
||||
|
||||
A full backup has to be turned back into a plain archive before it can be
|
||||
unpacked into `/data`, which the CLI does with the same password:
|
||||
@@ -359,10 +372,12 @@ but it can look like an open port in external scans.
|
||||
## Fixed image tag
|
||||
|
||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
||||
deliberately never `:latest`. This is a direct consequence of the backup
|
||||
version check above: the panel binary's embedded version and the image tag
|
||||
that produced it are the same value by construction (the release CI stamps
|
||||
both from one git tag — see `.github/workflows/release.yml`), so pinning the
|
||||
tag is what makes "restore into the same version" a checkable fact rather than
|
||||
deliberately never `:latest`. Until the `v1.0.0` cut the shipped pin is still
|
||||
`0.1.0` — intermediate CHANGELOG sections (`0.2.0`…`0.6.0`) record development
|
||||
cuts and do not imply a published image of that tag. Pinning matters because of
|
||||
the backup version check above: the panel binary's embedded version and the
|
||||
image tag that produced it are the same value by construction (the release CI
|
||||
stamps both from one git tag — see `.github/workflows/release.yml`), so the
|
||||
pin is what makes "restore into the same version" a checkable fact rather than
|
||||
a guess. Upgrade by bumping the tag deliberately, not by riding a moving
|
||||
target.
|
||||
|
||||
@@ -27,5 +27,6 @@ e2e/CI в [development.md](development.md), принятые риски в
|
||||
whitelist'ом), не могло быть разобрано getopt как флаг
|
||||
([internal/app/sasl.go](../internal/app/sasl.go)). Принятые риски в
|
||||
[security.md](security.md) не пополнились — существующие записи (origin-check
|
||||
fallback, отсутствие CSRF-токенов, send-log gap) покрывают всё найденное.
|
||||
Сводка ревизии — в записи `Security` CHANGELOG `[Unreleased]`.
|
||||
fallback, отсутствие CSRF-токенов, ложно-отрицательный `bounced` при сверке
|
||||
с `postqueue`) покрывают всё найденное.
|
||||
Сводка ревизии — в записи `Security` CHANGELOG `[0.5.0]`.
|
||||
|
||||
+5
-4
@@ -38,7 +38,7 @@ compose неделю ссылается на несуществующий обр
|
||||
|
||||
**Убрать `implementation-plan.md` — в релизном коммите.** Документ закрыт:
|
||||
уникального содержания в нём нет, § D (предрелизная ревизия безопасности)
|
||||
продублирован в [security.md](security.md) и CHANGELOG `[Unreleased]/Security`,
|
||||
продублирован в [security.md](security.md) и CHANGELOG `[0.5.0]/Security`,
|
||||
а разделы B.1–B.3 и C.4 вырезаны ещё в `22f86d1`. Держится до тега только
|
||||
потому, что описывает релизный гейт, пока тот формально не закрыт. При резке
|
||||
версии:
|
||||
@@ -137,7 +137,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` и `v1.x-closure-plan.md` удал
|
||||
|
||||
## Роль администратора домена — кандидат на 2.x
|
||||
|
||||
**Что это.** Сейчас в панели ровно один субъект: `requireAuth` — булев гейт, а не роль ([web.go:182](../internal/web/web.go:182)), сессия не несёт ничего, кроме факта входа. Роль выдаёт доступ к одному домену и только к нему: приложения этого домена (создание, режим отправителя, перегенерация пароля, удаление, свой L2-лимит), DKIM/DNS-статус домена и журнал отправки, отфильтрованный по домену — фильтр в журнале уже есть ([handlers_monitor.go:49](../internal/web/handlers_monitor.go:49)). Вне роли остаётся то, что глобально по своей природе: добавление и удаление доменов, `/reload`, полный бэкап (это весь `/data` вместе с `sasldb2`, то есть все домены сразу), очередь и хвост `mail.log` — они серверные и к домену не привязаны.
|
||||
**Что это.** Сейчас в панели ровно один субъект: `requireAuth` — булев гейт, а не роль ([web.go](../internal/web/web.go) — обёртка `mux.Handle("/", s.requireAuth(authed))`), сессия не несёт ничего, кроме факта входа. Роль выдаёт доступ к одному домену и только к нему: приложения этого домена (создание, режим отправителя, перегенерация пароля, удаление, свой L2-лимит), DKIM/DNS-статус домена и журнал отправки, отфильтрованный по домену — фильтр в журнале уже есть ([sendLogData](../internal/web/handlers_monitor.go)). Вне роли остаётся то, что глобально по своей природе: добавление и удаление доменов, `/reload`, полный бэкап (это весь `/data` вместе с `sasldb2`, то есть все домены сразу), очередь и хвост `mail.log` — они серверные и к домену не привязаны.
|
||||
|
||||
**Почему 2.x, а не v1.x.** [product.md](product.md) относит «несколько пользователей
|
||||
панели, роли» к out of scope (один администратор), поэтому появление второго
|
||||
@@ -172,8 +172,9 @@ README, `architecture.md`; [roadmap.md](roadmap.md) — внутренний т
|
||||
|
||||
## Разбиение `internal/web` на подпакеты — кандидат на 2.x
|
||||
|
||||
**Что это.** `internal/web` — самый крупный пакет проекта: 47 файлов, ~3030
|
||||
строк, в одной плоскости лежат хендлеры всех разделов панели, сессии,
|
||||
**Что это.** `internal/web` — самый крупный пакет проекта: ~50 файлов
|
||||
(включая шаблоны и static), ~25 `.go` / ~4000 строк Go, в одной плоскости
|
||||
лежат хендлеры всех разделов панели, сессии,
|
||||
security-заголовки, проверка Origin, валидация форм и рендер шаблонов.
|
||||
Кандидаты на выделение — `web/handlers` и `web/auth`, либо разрез по доменам
|
||||
панели.
|
||||
|
||||
Reference in New Issue
Block a user