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:
+1
-1
@@ -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
@@ -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**
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user