release: 1.6.0
test / test (push) Waiting to run

Add 30-day send statistics and auto level-2 rate limits on the domain page. Close Unreleased; pin compose and docs to 1.6.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 22:26:51 +03:00
parent 8538b8d5f6
commit c1ec4fbd79
27 changed files with 1092 additions and 140 deletions
+1 -1
View File
@@ -102,7 +102,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.5.0`; adjust if semver changes mid-track):
**Planned version cuts** (from pin `1.6.0`; adjust if semver changes mid-track):
| Stage | ID | Cut |
|---|---|---|
+18 -5
View File
@@ -169,7 +169,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.5.0`. Intermediate
deliberately never `:latest`. The current pin is `1.6.0`. 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
@@ -719,6 +719,18 @@ application ceiling and skip the domain check. Other IPs stay under the
domain limit (or level 1 alone). An application override without trusted
IPs is inactive.
**Manual and auto mode** — each domain and application limit can be
**Manual** (you set the ceiling and window) or **Auto**. Auto derives
`max_messages` from sending statistics: `ceil(average msg/h × multiplier)`
over the level-1 window (`RATE_LIMIT_WINDOW_SECONDS`), capped at level 1.
The panel shows 30-day statistics (total, peak and average msg/h) on the
domain page; level-1 refusals are not in the send log, so totals
under-count strict IP limits. When retention is below 30 days, statistics
use `min(30, retention)` days. With no traffic in the window, auto stays
inactive until messages are sent. Auto limits are recalculated every six
hours and on demand via **Recalculate now**. Application auto ceilings stay
strictly above an active domain limit when possible.
**Level 2 is best-effort, not a guarantee.** It runs inside the
journal-milter and is deliberately fail-open: if the rate-limit lookup hits
a store error, or the connecting client's IP is not available to the
@@ -750,10 +762,11 @@ default until it is changed there.
### Exporting and importing a single domain
Domain page → *Export domain* to write the file, *Backup* → *Import a
domain* to read it back in. This moves one domain — its DKIM key and its
applications' **working** SASL passwords — to a different SelfPost instance
without regenerating anything, so DNS (the DKIM TXT record) doesn't need to
change. Unlike a full restore (see [Full backup and
domain* to read it back in. This moves one domain — its DKIM key, its
applications' **working** SASL passwords, and configured **rate limits**
(mode, ceilings, multipliers, trusted IPs) — to a different SelfPost
instance without regenerating anything, so DNS (the DKIM TXT record)
doesn't need to change. Unlike a full restore (see [Full backup and
restore](#full-backup-and-restore)), this works across different
hostnames/instances. *Import* is global-administrator only; *export* is
available to any user who can access the domain, **including a domain-admin**
+12 -12
View File
@@ -1,6 +1,6 @@
# Plan: domain-stats-auto-ratelimit
**Status:** candidate
**Status:** done — shipped in `[1.6.0]` (2026-08-18); security review (Fable) pending
**Date:** 2026-08-17
**Version:** `1.x` MINOR; migrations must stay compatible with `1.0.0`.
@@ -192,14 +192,14 @@ Target version cut: **`1.6.0`** (MINOR). One commit per step; code only after
roadmap status is **agreed**. See [development.md](../development.md) § Plan
checklists.
- [ ] Migration `0006_rate_limit_auto.sql` (`mode`, `auto_multiplier`, `auto_updated_at`) — **Opus**
- [ ] `internal/store/stats.go`: total / peak / avg over 30 days — **Opus**
- [ ] Auto recalc job (6h): `ceil(avg × multiplier)`, L1 cap, fail-open on error — **Opus**
- [ ] Extend `RateLimit` + handler forms (manual/auto) — **Opus**
- [ ] Domain page stats card + per-app stats (`domain_detail.html`) — **Sonnet**
- [ ] Domain export JSON includes rate limits — **Opus**
- [ ] Milter tests — enforced limit matches stored ceiling — **Opus**
- [ ] Store and handler tests — **Sonnet**
- [ ] [guide.md](../guide.md) — **Sonnet**
- [ ] Security review rate-limit path — **Fable**
- [ ] `go vet`, `go test` on touched packages — **Haiku**
- [x] Migration `0007_rate_limit_auto.sql` (`mode`, `auto_multiplier`, `auto_updated_at`) — **Opus**
- [x] `internal/store/stats.go`: total / peak / avg over 30 days — **Opus**
- [x] Auto recalc job (6h): `ceil(avg × multiplier)`, L1 cap, fail-open on error — **Opus**
- [x] Extend `RateLimit` + handler forms (manual/auto) — **Opus**
- [x] Domain page stats card + per-app stats (`domain_detail.html`) — **Sonnet**
- [x] Domain export JSON includes rate limits — **Opus**
- [x] Milter tests — enforced limit matches stored ceiling — **Opus**
- [x] Store and handler tests — **Sonnet**
- [x] [guide.md](../guide.md) — **Sonnet**
- [ ] Security review rate-limit path — **Fable** (pending post-release)
- [x] `go vet`, `go test` on touched packages — **Haiku**
+3 -4
View File
@@ -59,10 +59,9 @@ The **domain-admin** role ships in the current line (global administrator plus
domain administrators with assigned domains). The optional **inbound relay**
(backup-MX / forwarder on port 25) ships in `[1.4.0]`, off by default behind
`INBOUND_RELAY_ENABLE`; it is relay/forward, not IMAP/webmail. **Send-log
retention in Settings** ships in `[1.5.0]`. Items marked *candidate* in the
[roadmap](roadmap.md) require explicit approval before coding (including
**30-day send statistics** and **auto level-2 rate limits** — see linked
plans there).
retention in Settings** ships in `[1.5.0]`. **30-day send statistics** and
**auto level-2 rate limits** ship in `[1.6.0]`. Items marked *candidate* in the
[roadmap](roadmap.md) require explicit approval before coding.
---
+6 -27
View File
@@ -28,18 +28,17 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| ID | Topic | Status | Progress | Plan |
|---|---|---|---|---|
| domain-stats-auto-ratelimit | 30-day send stats + auto level-2 rate limit | candidate | 0/11 | [plans/domain-stats-auto-ratelimit.md](plans/domain-stats-auto-ratelimit.md) |
| contributing | `CONTRIBUTING.md` | candidate | — | — |
| dmarc-reports | DMARC aggregate report ingestion and panel UI | candidate | 0/8 | [plans/dmarc-reports.md](plans/dmarc-reports.md) |
| panel-docs | In-panel operator documentation | candidate | 0/6 | [plans/panel-docs.md](plans/panel-docs.md) |
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
**Recommended order** (not binding): the next feature is
**domain-stats-auto-ratelimit** once agreed (panel retention ≥ 30 days helps
the stats window). send-log-retention shipped in
[CHANGELOG.md](../CHANGELOG.md) `[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 **dmarc-reports** once
agreed. domain-stats-auto-ratelimit shipped in
[CHANGELOG.md](../CHANGELOG.md) `[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
@@ -48,26 +47,6 @@ is `done/total` checklist steps in that plan ([development.md](development.md)
---
## domain-stats-auto-ratelimit
**Goal:** 30-day sending statistics per domain and application (total, peak and
average msg/h) on the domain page, plus optional **auto** level-2 rate limits
computed as `ceil(avg_hourly × multiplier)` over the level-1 window.
**Boundary:** extension of v1.0 manual L2 limits; does not change Postfix
level 1. Stats exclude level-1 refusals (not in send log). Auto with zero
traffic stays inactive.
**Done when:** see
[plans/domain-stats-auto-ratelimit.md](plans/domain-stats-auto-ratelimit.md).
**Dependencies / risks:** send log data and preferably
[send-log-retention](#send-log-retention) ≥ 30 days; aggregate query cost on
large logs; rate-limit path needs security review.
**Version:** `1.x` MINOR; `candidate` until explicitly agreed.
---
## contributing
**Goal:** `CONTRIBUTING.md` in the root — the dev loop, the checks to run