diff --git a/CHANGELOG.md b/CHANGELOG.md index ddad24f..142c338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Changed + +- Operator and as-built docs aligned with the code after a full pass — + [architecture.md](docs/architecture.md) (port 25 opens for inbound relay + and/or DMARC ingest; antispam requires inbound relay; DMARC pipe transport + and map paths; `/help` and `/dmarc*` routes; four panel roles including + rate-limit recalc; restore Resync vs manual reload); [guide.md](docs/guide.md) + and [README.md](README.md) (image pin `1.9.1`, port 25 conditions, combined + listener limits, reload vs Resync); [roadmap.md](docs/roadmap.md) (`panel-docs` + closed at `[1.8.0]`, next item inbound-antispam-panel); [development.md](docs/development.md) + version-cut table through `1.9.1`; [schema-migrations.md](docs/schema-migrations.md) + last-updated note. No behaviour change. + ## [1.9.1] - 2026-08-19 Port-25 Postfix startup when inbound relay, DMARC ingest, or the inbound antispam diff --git a/README.md b/README.md index 96d72d1..89c2cd0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ send log and DNS checks in the panel, encrypted backups. - Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing - Optional inbound relay on port 25 (backup-MX / forwarder; off by default) +- Optional DMARC aggregate report ingest on port 25 (off by default) +- In-panel Help drawer and `/help` page for Status and domain controls +- Per-application client IP allow-list for SMTP AUTH (optional) - Web panel — domains, applications, deliveries, mail queue, system log, backup - Multi-domain relay — each SASL application is bound to one sending domain - DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check @@ -108,7 +111,7 @@ docker run --rm -d --name selfpost-try \ -e SELFPOST_HOSTNAME=mail.local.test \ -e PANEL_COOKIE_SECURE=false \ -v selfpost-try-data:/data \ - ghcr.io/mixeme/selfpost:1.7.0 + ghcr.io/mixeme/selfpost:1.9.1 ``` **Get the setup URL** (pick one): @@ -149,8 +152,9 @@ deployment](docs/guide.md#full-deployment) section, with proxy-specific commands under [Reverse proxy](docs/guide.md#reverse-proxy-mandatory). The compose file always publishes **465**, **587**, and **25**; Postfix listens -on 587 only when `SUBMISSION_ENABLE=true`, and on 25 only when -`INBOUND_RELAY_ENABLE=true` (see [Ports](docs/guide.md#ports)). Bump the +on 587 only when `SUBMISSION_ENABLE=true`, and on 25 when +`INBOUND_RELAY_ENABLE=true` and/or `DMARC_REPORTS_ENABLE=true` (see +[Ports](docs/guide.md#ports)). Bump the pinned image tag deliberately when upgrading, never `:latest` ([why](docs/guide.md#fixed-image-tag)). Optional variables (`TRUSTED_PROXY_CIDR`, rate limits, retention): see [Environment variables](docs/guide.md#environment-variables). diff --git a/docs/architecture.md b/docs/architecture.md index d99ee35..85377eb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,7 +27,7 @@ Managed programs ([build/supervisord.conf](../build/supervisord.conf)): | Program | User | Priority | Role | |---|---|---|---| | `opendkim` | root → `opendkim` (`UserID` in opendkim.conf) | 100 | DKIM signing milter | -| `panel` | panel | 200 | HTTP UI + journal-milter + log-tailer goroutine | +| `panel` | panel | 200 | HTTP UI + journal-milter + log-tailer + rate-limit recalc | | `postfix` | root (wrapper) | 300 | MTA — started only after both milter sockets exist | | `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) | | `cert-reload` | root | 400 | Daily `postfix reload` for renewed TLS certs | @@ -54,30 +54,40 @@ Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission) ├─► journal-milter (send log + L2 rate limits, fail-open) └─► outbound MX delivery (port 25 client) -Internet ──► Postfix smtp inet :25 (only when INBOUND_RELAY_ENABLE=true) +Internet ──► Postfix smtp inet :25 (when INBOUND_RELAY_ENABLE=true + │ and/or DMARC_REPORTS_ENABLE=true) │ - ├─► optional antispam milter (inbound only) - └─► smtp:[upstream]:port (transport_maps; no local delivery) + ├─► optional antispam milter (inbound relay only) + ├─► DMARC aggregate ingest (pipe → dmarc-ingest) + └─► smtp:[upstream]:port (inbound relay only; + transport_maps; no local delivery) ``` -The inbound listener is **absent** when the flag is off (`postconf -MX smtp/inet` -removes Debian's stock smtpd). Outbound delivery still uses the `smtp unix` -client; it is not the same service. +The smtp/inet listener is **absent** when both flags are off (`postconf -MX +smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the +`smtp unix` client; it is not the same service. ### Postfix ([build/postfix-config.sh](../build/postfix-config.sh)) - **465/smtps** — implicit TLS, SASL required; primary listener. - **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with `smtpd_tls_security_level=encrypt`. -- **25/smtp inet** — only when `INBOUND_RELAY_ENABLE=true`. No SASL, no - OpenDKIM, no journal-milter. Accepts only `relay_domains` + - `relay_recipient_maps` (`reject_unauth_destination`, - `reject_unlisted_recipient`). Maps under `/data/postfix/` - (`relay_domains`, `transport`, `relay_recipients`, `tls_policy`), written - atomically by [internal/postfix/inbound.go](../internal/postfix/inbound.go). - Domains with an empty upstream host are omitted from the maps. Optional - `INBOUND_ANTISPAM_MILTER` on this listener only; default - `milter_default_action` is fail-open (`accept`). +- **25/smtp inet** — when `INBOUND_RELAY_ENABLE=true` and/or + `DMARC_REPORTS_ENABLE=true`. No SASL, no OpenDKIM, no journal-milter. + **Inbound relay** (`INBOUND_RELAY_ENABLE=true`): accepts only + `relay_domains` + `relay_recipient_maps` (`reject_unauth_destination`, + `reject_unlisted_recipient`). Maps under `/data/postfix/` (`relay_domains`, + `transport`, `relay_recipients`, `tls_policy`), written atomically by + [internal/postfix/inbound.go](../internal/postfix/inbound.go). Domains with + an empty upstream host are omitted from the maps. Optional + `INBOUND_ANTISPAM_MILTER` only when inbound relay is enabled (not on + DMARC-only port 25); default `milter_default_action` is fail-open + (`accept`). **DMARC ingest** (`DMARC_REPORTS_ENABLE=true`): allow-listed + report addresses via `check_recipient_access` on `postfix/dmarc_recipients`; + messages pipe to `dmarc-ingest` (symlink to `panel`). When both flags are + on, one listener serves both paths; per-IP rate uses + `INBOUND_RATE_LIMIT_MESSAGES_PER_IP` and message size uses the greater of + `INBOUND_MESSAGE_SIZE_LIMIT` and `DMARC_MESSAGE_SIZE_LIMIT`. - **No open relay** — `permit_sasl_authenticated`, `reject_unauth_destination`; `smtpd_sender_login_maps` + `reject_sender_login_mismatch`. - **Level-1 rate limit** — `smtpd_client_message_rate_limit` / @@ -93,7 +103,7 @@ by the panel. Socket `/run/opendkim/opendkim.sock`. ### Panel binary ([cmd/panel](../cmd/panel)) -One process, three roles: +One process, four roles: 1. **HTTP server** — `:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse proxy only. On start it runs `postconf -h` once for the deferred-mail retry @@ -116,6 +126,8 @@ One process, three roles: 3. **log-tailer** — follows `MAIL_LOG`, updates send-log delivery status by queue-id. Send-log `queued → sent` transitions depend on this goroutine alone (`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)). +4. **rate-limit recalc** — every six hours (and on demand from the domain + page), recomputes level-2 **Auto** rate limits from send-log statistics. Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure). @@ -200,11 +212,13 @@ below is a summary — HTMX fragment endpoints | `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins | | `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view; retry-policy card on the page (not the HTMX fragment) | | `/system-log`, `/system-log/*` | **Global.** `mail.log` tail | -| `/reload` | **Global.** `POST` — reload OpenDKIM + Postfix maps | +| `/reload` | **Global.** `POST` — reload OpenDKIM tables, Postfix sender map, and inbound relay maps when enabled (not DMARC maps — see § Persistence restore) | | `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) | +| `/help` | In-panel operator help (any authenticated user) | | `/settings` | Username/password for any user; DMARC report default is **global** only | | `/users`, `/users/*` | **Global.** Panel user CRUD | | `/inbound`, `/inbound/{id}`, `/inbound/{id}/*` | **Global.** Inbound relay domains. Registered only when `INBOUND_RELAY_ENABLE=true`; otherwise 404. | +| `/dmarc`, `/dmarc/reports/{id}`, `/dmarc/domains/{id}` | **Global** list and report detail; domain roll-up scoped like deliveries. Registered only when `DMARC_REPORTS_ENABLE=true`; otherwise 404. | HTMX polling refreshes monitoring fragments (5 s while the operator is active on the page, 30 s when the tab is visible but idle, none when hidden — scheduled in @@ -271,6 +285,7 @@ flowchart TB domainSvc["internal/domain"] appSvc["internal/app"] inboundSvc["internal/inbound"] + dmarcSvc["internal/dmarc"] end subgraph persistence ["Persistence"] store["internal/store — SQLite, embedded migrations"] @@ -293,6 +308,7 @@ flowchart TB web --> domainSvc web --> appSvc web --> inboundSvc + web --> dmarcSvc web --> backupPkg web --> dnscheck web --> health @@ -300,15 +316,18 @@ flowchart TB domainSvc --> store appSvc --> store inboundSvc --> store + dmarcSvc --> store milterPkg --> store logtail --> store domainSvc --> postfix appSvc --> postfix inboundSvc --> postfix + dmarcSvc --> postfix ``` -The three roles inside the `panel` process (HTTP server, journal-milter, -log-tailer goroutine) share one binary and one SQLite handle on purpose — see +The four roles inside the `panel` process (HTTP server, journal-milter, +log-tailer, rate-limit recalc) share one binary and one SQLite handle on +purpose — see [Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the single-connection trade-off that follows from it. @@ -327,6 +346,9 @@ single-connection trade-off that follows from it. | `postfix/transport` | Inbound next-hop `smtp:[host]:port` | | `postfix/relay_recipients` | Inbound recipient allow-list or `@domain` catch-all | | `postfix/tls_policy` | TLS policy for inbound next hops | +| `postfix/dmarc_recipients` | Allow-listed DMARC aggregate report addresses | +| `postfix/dmarc_transport` | Pipe transport for report ingest | +| `postfix/dmarc_relay_domains` | Domains accepted for DMARC report delivery | | `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate | | `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) | | `manifest.json` | Backup version stamp (consumed on restore) | @@ -344,10 +366,12 @@ under `/data`), `docker-compose.yml`, `.env`, and `certs/` when present; version check on restore. Requires the project directory mounted read-only at `SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the first successful boot after restore, the panel runs one **Resync** — OpenDKIM's -tables, Postfix's sender map, and (when `INBOUND_RELAY_ENABLE=true`) inbound -relay maps are re-derived from SQLite and both daemons are reloaded, so drift -between the extracted archive and the database is healed before mail flows -(same step as `POST /reload` on demand). Stopped-container +tables, Postfix's sender map, inbound relay maps (when +`INBOUND_RELAY_ENABLE=true`), and DMARC maps (when `DMARC_REPORTS_ENABLE=true`) +are re-derived from SQLite and both daemons are reloaded, so drift between the +extracted archive and the database is healed before mail flows. Manual +`POST /reload` on the Status page resyncs OpenDKIM, the sender map, and +inbound maps only — not DMARC maps. Stopped-container `tar` of `./data` alone remains possible for state-only copies (see guide). **Optional encryption** of the two secret-bearing downloads @@ -398,6 +422,9 @@ unsupported rather than as a missing doc: `POSTFIX_TLS_POLICY_MAPS` (`/data/postfix/tls_policy`) — same desync if overridden without matching the panel writer in [internal/postfix/inbound.go](../internal/postfix/inbound.go), + `POSTFIX_DMARC_RECIPIENTS` (`/data/postfix/dmarc_recipients`), + `POSTFIX_DMARC_TRANSPORT` (`/data/postfix/dmarc_transport`), + `POSTFIX_DMARC_RELAY_DOMAINS` (`/data/postfix/dmarc_relay_domains`), `POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — set in `build/postfix-config.sh`), `SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for full backups; mount `.:/selfpost-deploy:ro` in compose). diff --git a/docs/development.md b/docs/development.md index 50edd3c..867affe 100644 --- a/docs/development.md +++ b/docs/development.md @@ -107,7 +107,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com 2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see [§ Release image](#release-image)). -**Planned version cuts** (from pin `1.7.0`; adjust if semver changes mid-track): +**Planned version cuts** (from pin `1.9.1`; adjust if semver changes mid-track): | Stage | ID | Cut | |---|---|---| @@ -117,6 +117,9 @@ than the default «meaningful step» rule in [§ Commits and release build](#com | 4 | domain-stats-auto-ratelimit | `1.6.0` | | 5 | dmarc-reports | `1.7.0` | | 6 | panel-docs | `1.8.0` | +| 7 | application auth IPs | `1.9.0` | +| 8 | port-25 Postfix startup fix | `1.9.1` (PATCH) | +| 9 | inbound-antispam-panel | `1.10.0` (planned) | Docs-only prep (checklists in plans, no product code) uses the same per-step commit rule but **no** version cut until the next product stage ships. diff --git a/docs/guide.md b/docs/guide.md index 0e90656..7360d48 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -52,9 +52,10 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications). 465 (smtps) is always active. Port **587** is published even when `SUBMISSION_ENABLE=false`; nothing listens until you set it to `true`. Port **25** is published even when `INBOUND_RELAY_ENABLE=false`; Postfix does not -accept inbound mail until you set it to `true` (see [Inbound -relay](#inbound-relay)). Harmless extra publishes can look like open ports in -external scans. +listen on port 25 until you set **`INBOUND_RELAY_ENABLE=true`** and/or +**`DMARC_REPORTS_ENABLE=true`** (see [Inbound relay](#inbound-relay) and +[DMARC reports](#dmarc-reports)). Harmless extra publishes can look like open +ports in external scans. ### Local trial @@ -172,7 +173,7 @@ cat ./data/setup-token #### Fixed image tag `deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`), -deliberately never `:latest`. The current pin is `1.7.0`. Intermediate +deliberately never `:latest`. The current pin is `1.9.1`. Intermediate CHANGELOG sections (`0.2.0`…`0.6.0`) record development cuts from before that image was published. Pinning matters because of the backup version check (see [Full backup and restore](#full-backup-and-restore)): the panel binary's @@ -194,12 +195,12 @@ expected to set; defaults match the code exactly. | `SUBMISSION_ENABLE` | When `true`, also listen on port 587 with STARTTLS (RFC 6409 submission) alongside the primary 465/smtps listener. | `false` | `.env` | | `INBOUND_RELAY_ENABLE` | When `true`, accept mail on port 25 for domains configured under *Inbound* in the panel and forward them to the upstream you set. Off by default — the outbound path is unchanged. See [Inbound relay](#inbound-relay). | `false` | `.env` | | `DMARC_REPORTS_ENABLE` | When `true`, accept DMARC aggregate reports on port 25 only for report addresses configured in the panel, parse gzip/XML, and show summaries under *DMARC*. Off by default. See [DMARC reports](#dmarc-reports). | `false` | `.env` | -| `DMARC_RATE_LIMIT_MESSAGES_PER_IP` | Per-client-IP cap on port 25 when DMARC ingest is on (shared listener with inbound relay if both are enabled). | `20` | `.env` | +| `DMARC_RATE_LIMIT_MESSAGES_PER_IP` | Per-client-IP cap on port 25 when DMARC ingest is on. When inbound relay is also enabled, **`INBOUND_RATE_LIMIT_MESSAGES_PER_IP`** applies to the shared listener instead. | `20` | `.env` | | `DMARC_MESSAGE_SIZE_LIMIT` | Maximum report message size in bytes when DMARC ingest is on. | `5242880` (5 MiB) | `.env` | -| `INBOUND_ANTISPAM_MILTER` | Optional milter on the inbound listener only (not 465/587). Empty = off. Format `inet:host:port` or `unix:/path`. Example with [deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml): `inet:antispam:11332`. | *(empty)* | `.env` | +| `INBOUND_ANTISPAM_MILTER` | Optional milter on the inbound relay listener only (requires `INBOUND_RELAY_ENABLE=true`; not 465/587 or DMARC-only port 25). Empty = off. Format `inet:host:port` or `unix:/path`. Example with [deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml): `inet:antispam:11332`. | *(empty)* | `.env` | | `INBOUND_ANTISPAM_MILTER_ACTION` | What Postfix does if that milter is down: `accept` (fail-open) or `tempfail` (defer). | `accept` | `.env` | | `INBOUND_RATE_LIMIT_MESSAGES_PER_IP` | Coarse per-client-IP cap on inbound smtpd (`smtpd_client_message_rate_limit`). Uses the same window as `RATE_LIMIT_WINDOW_SECONDS`. | `20` | `.env` | -| `INBOUND_MESSAGE_SIZE_LIMIT` | Maximum message size in bytes on inbound smtpd (`message_size_limit`). | `26214400` (25 MiB) | `.env` | +| `INBOUND_MESSAGE_SIZE_LIMIT` | Maximum message size in bytes on inbound smtpd (`message_size_limit`). When both inbound relay and DMARC ingest are on, the shared listener uses the **greater** of this and `DMARC_MESSAGE_SIZE_LIMIT`. | `26214400` (25 MiB) | `.env` | | `RATE_LIMIT_MESSAGES_PER_IP` | Level-1 backstop: maximum messages one client IP may submit per window (Postfix `smtpd_client_message_rate_limit`). See [Rate limiting — level 1](#rate-limiting--level-1-ip-backstop). | `100` | `.env` | | `RATE_LIMIT_WINDOW_SECONDS` | Level-1 window length in seconds (Postfix `anvil_rate_time_unit`). | `3600` | `.env` | | `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows — the main driver of `/data` growth over time. After the first panel start, change retention on **Settings** (global administrator); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` | @@ -342,7 +343,8 @@ against the PTR record the internet publishes for this server's IP DNS. The **Reload configuration** button re-applies OpenDKIM tables and the Postfix sender map from the database (and inbound relay maps when `INBOUND_RELAY_ENABLE=true`) — use it if daemons drifted from what the panel -shows after manual edits under `/data`. +shows after manual edits under `/data`. It does **not** rebuild DMARC Postfix +maps; a full restore Resync does (see [Restore](#restore)). ### Mail queue and System log @@ -376,8 +378,8 @@ restart). Application SASL logins are separate and are not changed here. There are two roles: - **Global administrator** — full access to every page and every domain, - including Users, Backup, Status, Mail queue, System log, and Inbound (when - the inbound relay flag is on). + including Users, Backup, Status, Mail queue, System log, Help, Inbound (when + the inbound relay flag is on), and DMARC (when `DMARC_REPORTS_ENABLE=true`). - **Domain-admin** — scoped to one or more domains assigned by a global administrator. Sees only those domains' pages, applications, and Deliveries rows; cannot add or delete domains. `/users`, `/backup`, @@ -547,10 +549,11 @@ refuses to start otherwise and tells you which tag to use. On the first successful start after restore, `data/manifest.json` from the archive is **deleted** — it guards only that one boot, so a later in-place upgrade is not blocked. On that same first boot the panel also runs one **Resync** — OpenDKIM's -tables and Postfix's sender map are re-derived from SQLite (and inbound relay -maps when `INBOUND_RELAY_ENABLE=true`) and both daemons are reloaded, healing -any drift between the extracted files and the database (the Status page's -*Reload configuration* button runs the same step on demand). This is why the +tables and Postfix's sender map are re-derived from SQLite (inbound relay maps +when `INBOUND_RELAY_ENABLE=true`; DMARC maps when `DMARC_REPORTS_ENABLE=true`) +and both daemons are reloaded, healing any drift between the extracted files and +the database. The Status page's *Reload configuration* button resyncs OpenDKIM, +the sender map, and inbound maps only — not DMARC maps. This is why the compose file pins a fixed tag rather than `:latest`: without a known version, there'd be no way to tell which image restoring a given backup actually requires (see [Fixed image tag](#fixed-image-tag)). @@ -688,8 +691,10 @@ you configure. It is **not** mailboxes, IMAP, or webmail — SelfPost never stores the message locally. **Off by default.** Set `INBOUND_RELAY_ENABLE=true` in `.env` and recreate the -container. Until then there is no `smtp inet` listener, no Inbound item in -the nav, and `/inbound` is 404. Outbound 465/587 is unchanged. +container. Until then there is no inbound-relay `smtp inet` listener, no +*Inbound* item in the nav, and `/inbound` is 404. (Port 25 can still listen +when only `DMARC_REPORTS_ENABLE=true` — see [DMARC reports](#dmarc-reports).) +Outbound 465/587 is unchanged. **Panel** (`/inbound`, global administrator only): add a domain, set the upstream host/port and TLS to that hop (opportunistic / required / off), and @@ -710,7 +715,8 @@ Prefer an explicit recipient list so unknown addresses are refused at RCPT and never generate a bounce (backscatter). **Anti-spam.** SelfPost does not ship a filter. To attach one, set -`INBOUND_ANTISPAM_MILTER` (inbound listener only) and merge +`INBOUND_ANTISPAM_MILTER` (requires inbound relay enabled; not on DMARC-only +port 25) and merge [deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml) the same way as the nginx/Caddy fragments: diff --git a/docs/roadmap.md b/docs/roadmap.md index 40a6343..5fc1cd5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -29,18 +29,17 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | ID | Topic | Status | Progress | Plan | |---|---|---|---|---| | contributing | `CONTRIBUTING.md` | candidate | — | — | -| panel-docs | In-panel operator documentation | agreed | 6/6 | [plans/panel-docs.md](plans/panel-docs.md) | | inbound-antispam-panel | Inbound antispam journal + allow/deny lists | agreed | 1/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) | | inbound-quarantine | Inbound spam quarantine (hold / review / release) | candidate | — | [plans/inbound-quarantine.md](plans/inbound-quarantine.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — | -**Recommended order** (not binding): the next feature is **panel-docs** once -agreed. dmarc-reports shipped in -[CHANGELOG.md](../CHANGELOG.md) `[1.7.0]` (security review of the ingest path -pending); domain-stats-auto-ratelimit in `[1.6.0]`; send-log-retention in -`[1.5.0]`; inbound-relay in `[1.4.0]`; queue-retries in `[1.3.1]`; the -2026-08-13 full-tree review follow-ups are in `[1.3.0]`. Candidates need -explicit agreement before they join the queue. +**Recommended order** (not binding): the next feature is +**inbound-antispam-panel** (`1.10.0`). panel-docs shipped in +[CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]` +(security review of the ingest path pending); domain-stats-auto-ratelimit in +`[1.6.0]`; send-log-retention in `[1.5.0]`; inbound-relay in `[1.4.0]`; +queue-retries in `[1.3.1]`; the 2026-08-13 full-tree review follow-ups are in +`[1.3.0]`. Candidates need explicit agreement before they join the queue. After a context reset, pick an item marked `agreed` or `in progress`, then work the **Implementation checklist** in its linked plan. The `Progress` column above @@ -69,6 +68,9 @@ priority. ## panel-docs +**Status:** done — shipped in `[1.8.0]` (2026-08-18). Plan: +[plans/panel-docs.md](plans/panel-docs.md) (checklist complete). + **Goal:** built-in operator documentation in the panel — short pages (or a help drawer) that explain what each Status check and other controls mean, without sending the operator out to `docs/guide.md`. @@ -87,7 +89,7 @@ read the git tree for day-to-day meaning of a card. **Dependencies / risks:** copy ownership and translation; keeping help in sync when checks change; not bloating every page with a second column of prose. -**Version:** `1.x` MINOR; `candidate` until explicitly agreed. +**Version:** shipped `1.8.0`. --- diff --git a/docs/schema-migrations.md b/docs/schema-migrations.md index ffcc07b..f876281 100644 --- a/docs/schema-migrations.md +++ b/docs/schema-migrations.md @@ -23,7 +23,8 @@ Documentation. | Database file | `/data/selfpost.db` (bind mount) | | Compatibility | 1.x MINOR releases must boot a `1.0.0` data directory | -Last updated with release **1.9.0** (`0009_application_auth_ips`). +Last updated with release **1.9.0** (`0009_application_auth_ips`); **1.9.1** +had no schema change. ---