docs: remove completed plans and trim the open-work tracker
test / test (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 01:34:04 +03:00
parent 7cc499d57c
commit 5eaf665c01
7 changed files with 23 additions and 618 deletions
+5
View File
@@ -37,6 +37,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
The three
WOFF2 files add ~76 KB to the image and are served from the panel's own origin,
so the Content-Security-Policy is unchanged (`default-src 'self'`).
- docs: [roadmap.md](docs/roadmap.md) and [product.md](docs/product.md) no
longer list domain-admin or visual-style as open work — both ship in this line.
Completed plan files (`domain-admin`, `visual-style`, `web-split`) are
removed; history stays in git and the entries above. Inbound relay is the
main agreed 1.x+ item left on the roadmap.
## [1.1.0] - 2026-08-10
-259
View File
@@ -1,259 +0,0 @@
# Plan: domain-admin (domain administrator role)
**Status:** agreed
**Version:** target bump **1.x** MINOR, given a compatible migration of the
current administrator into a global one.
**Order:** recommended after [web-split](web-split.md) (done), before
[inbound-relay](inbound-relay.md).
---
## What this is
Today the panel has exactly one subject: `RequireAuth` is a boolean gate, not a
role ([web.go](../../internal/web/web.go) — the
`mux.Handle("/", s.auth.RequireAuth(authed))` wrapper), and the session carries
nothing beyond the fact of being signed in.
Two panel roles:
| Role | Scope |
|------|-------|
| **global** | Full panel except nothing new — same powers as today's single admin |
| **domain_admin** | Only **assigned** domains (one or several; list set by global admin) |
For each assigned domain, a domain-admin can:
- applications (create, sender mode, password regeneration, delete, L2 limit);
- DKIM/DNS status and recheck;
- per-domain DMARC `rua=` (inherit / none / custom) — full control on the
domain page;
- send log filtered to assigned domains;
- domain export (encrypted `.spde` optional, same as today);
- domain-level L2 rate limit.
What stays **global-only** (domain-admin gets 404 or redirect):
- adding and removing domains;
- domain import;
- creating/editing/deleting panel users and assigning domains;
- `/reload`;
- full backup (`/backup` — all of `/data` including every domain's `sasldb2`);
- mail queue (`/mail-queue*`);
- system log tail (`/system-log*`);
- status page (`/status*`) — server-wide health, queue summary, reload, DNS
recheck of the **hostname**; same treatment as queue and system log.
Domain-admin **self-service** on `/account`: username and password only (not
global DMARC report email).
## Why this extends v1.0
[product.md](../product.md) puts "multiple panel users, roles" out of scope
(one administrator). A second subject is a deliberate widening of the project's
boundary, as inbound-relay is.
The cost is phase-sized, not patch-sized:
- a users table and their binding to domains;
- the role in the session;
- authorisation in every handler (not only on the route — today `{id}`/`{aid}`
are checked for nothing beyond existence);
- reworking first-run setup and password change for several users;
- accounting for the new subject in backup and domain export.
*(The earlier wording of this item — "2FA and multiple administrators" — has
been replaced: 2FA is off the table, and "multiple administrators" is narrowed
to one specific role, because what is needed is not a second all-powerful admin
but limited access for the owner of one or several domains, with the list set
by the global administrator.)*
---
## Schema and migration
**New migration** `0005_panel_users.sql`:
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('global', 'domain_admin')),
dmarc_report_email TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
);
CREATE TABLE user_domains (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, domain_id)
);
-- Migrate existing administrator → global user (idempotent guard via admin count).
INSERT INTO users (username, password_hash, role, dmarc_report_email, created_at)
SELECT username, password_hash, 'global', dmarc_report_email, created_at
FROM admin WHERE id = 1;
DROP TABLE admin;
```
**Sessions:** keep `sessions.username` (no schema change). On login and
`RequireAuth`, resolve username → `User` row (role + domain IDs). Stale session
after username change behaves as today (`Lookup` fails → redirect login).
**Backup/restore:** full backup already snapshots `selfpost.db` via
`VACUUM INTO`; users and bindings restore with the DB. No manifest format change
required (same `selfpost-full-backup`).
**DMARC two levels:**
- **Global** `users.dmarc_report_email` — only on global user's `/account`;
default `rua=` when a domain uses *inherit*.
- **Per-domain** `domains.dmarc_rua` — domain-admin edits on the domain page
(existing handler); domain-admin never sees the global default field.
---
## Principal model
Request context carries a `Principal` (in `internal/web/auth`):
```go
type Role string // "global" | "domain_admin"
type Principal struct {
ID int64
Username string
Role Role
Domains []int64 // assigned domain IDs; empty for global (meaning "all")
}
```
Helpers:
- `CurrentPrincipal(r)` — from context;
- `IsGlobal(p)``p.Role == "global"`;
- `CanAccessDomain(p, domainID)` — global or `domainID` in `p.Domains`;
- `CanAccessApp(p, app)``CanAccessDomain(p, app.DomainID)`.
`lookupDomain` / `lookupApplication` in handlers call `CanAccess*` after
existence check; return 404 (not 403) to avoid leaking IDs.
---
## Route matrix
| Method | Path | global | domain_admin |
|--------|------|--------|--------------|
| GET | `/` | → `/status` | → `/domains` |
| GET | `/status`, `/status/fragment` | yes | **no** (404) |
| POST | `/status/recheck` | yes | **no** |
| GET | `/domains` | all domains | assigned only |
| POST | `/domains` | yes | **no** |
| POST | `/domains/import` | yes | **no** |
| GET | `/domains/{id}` | yes | assigned |
| POST | `/domains/{id}/dns-recheck` | yes | assigned |
| GET/POST | `/domains/{id}/delete` | yes | **no** |
| POST | `/domains/{id}/applications` | yes | assigned |
| POST | `/domains/{id}/ratelimit` | yes | assigned |
| POST | `/domains/{id}/dmarc` | yes | assigned |
| POST | `/domains/{id}/export` | yes | assigned |
| POST | `/applications/{aid}/*` | yes | if app in assigned domain |
| POST | `/reload` | yes | **no** |
| GET/POST | `/account` | username, password, global DMARC email | username, password only |
| GET/POST | `/backup` | yes | **no** |
| GET | `/deliveries*` | all (optional filter) | clamped to assigned domains |
| GET | `/mail-queue*` | yes | **no** |
| GET | `/system-log*` | yes | **no** |
| GET | `/users` | list users | **no** |
| GET/POST | `/users/new` | create user | **no** |
| GET/POST | `/users/{uid}` | edit/delete user | **no** |
| POST | `/logout` | yes | yes |
**Deliveries:** for domain-admin, `sendLogData` forces filter to assigned
domain set; dropdowns list only assigned domains/apps; reject `domain` query
param outside assignment; `HandleDelivery` checks log row's `domain` field.
---
## User management UI (global only)
New routes under `/users`:
- **List** — username, role, assigned domain names (or "all" for global).
- **Create** — username, password, role (`domain_admin` default), multi-select
domains (required when role is `domain_admin`).
- **Edit** — change password (optional), reassign domains, delete user.
- **Guards:** cannot delete the last `global` user; cannot demote self to
`domain_admin` without another global user; domain-admin role cannot access
these routes.
Templates: `users.html`, `user_form.html`; nav link visible only for global
users.
---
## Auth / setup / sessions
- **Setup** (`/setup/{token}`): unchanged semantics — creates first **global**
user via `CreateGlobalUser`; `AdminExists``UserExists`.
- **Login:** authenticate against `users` by username + bcrypt.
- **Password change:** per-user `UpdateUser`; domain-admin cannot change
another user's password.
- **Session rename / destroy others:** unchanged behaviour keyed by username.
---
## Navigation
[layout.html](../../internal/web/view/templates/layout.html) `nav` template:
- **global:** all items today (status, domains, deliveries, mail queue, system
log, backup, settings) + **Users**.
- **domain_admin:** domains, deliveries, settings only.
Pass `IsGlobal` (or `Principal`) into every rendered page.
---
## Security
- **CSRF:** keep origin-check-only for now ([security.md](../security.md) ADR);
note in CHANGELOG that multi-user panel reopens the ADR — no CSRF tokens in
this phase.
- **Export encryption:** optional password on domain export remains; full backup
encryption trigger ("second administrator") is satisfied by domain-admin
existing — no change required.
- **Authorization tests:** table-driven tests for global vs domain-admin on
representative handlers; explicit `{aid}` cross-domain mutation blocked.
---
## Implementation order
1. Migration `0005_panel_users.sql` + `store/users.go` (CRUD, domain bindings).
2. Auth: login against `users`, `Principal` in context, setup creates global user.
3. `CanAccessDomain` / `CanAccessApp`; harden `lookupDomain` / `lookupApplication`.
4. Route guards: global-only middleware or per-handler checks.
5. Filter lists: dashboard, deliveries, domain detail DMARC inherit source.
6. User management handlers + templates.
7. Nav visibility + default redirect (`/`).
8. Tests + `go build` / `go vet` / `go test`; CHANGELOG `[Unreleased]`.
---
## Done when
- A global administrator and a domain-admin with different rights both work
through the panel; the domain-admin cannot reach past the **assigned**
domains;
- the current single admin migrates into a global one without losing access;
- backup/restore accounts for users and their bindings;
- `build`/`vet`/`test`/image green.
## Risks
- An incomplete `{id}`/`{aid}` check in a handler — access leaking to someone
else's domain;
- breaking setup or backup — that would be a semver major, not 1.x.
+3 -2
View File
@@ -3,8 +3,9 @@
**Status:** agreed
**Version:** target bump **1.x** MINOR; **`2.x` possible** — to be settled once
the implementation lands (do not fix a major in advance).
**Order:** recommended after [web-split](web-split.md) and
[domain-admin](domain-admin.md).
**Order:** recommended next among agreed 1.x+ items (after `internal/web` split
and the domain-admin role, both shipped — see [CHANGELOG](../CHANGELOG.md)
`[1.1.0]` and `[Unreleased]`).
---
-226
View File
@@ -1,226 +0,0 @@
# Plan: visual-style
**Status:** agreed
**Version:** no bearing on semver — presentation only, no schema and no route
changes.
**Order:** independent of the feature roadmap; may be taken up between feature
items.
---
## Goal
Bring the control panel's surface in line with the mark that was approved in
[selfpost-proof.html](../assets/selfpost-proof.html): its palette, its
typography, and the plainness of its components. Today the panel is a default
blue-on-cool-grey admin theme standing next to a warm brick stamp, so the mark
reads as pasted onto someone else's page.
## Scope
**In:**
- `internal/web/view/static/panel.css` — colour tokens, typography, spacing,
every component rule.
- `internal/web/view/static/` — three self-hosted font files.
- Templates, only where a class has to be added or a wrapper introduced for a
rule to have something to attach to.
- `NOTICE` — the OFL attribution the font files oblige.
**Out:**
- Any change to what a page does, which pages exist, or what an operator has to
click. No new features, no copy rewriting.
- The navigation's position and the two-column shell. The proof's panel mock
shows a horizontal bar on a dark header; the panel's left column also carries
the per-page section index (`.sections` plus the scroll-spy in `panel.js`),
which that layout has nowhere to put. Keeping the column is a deliberate
divergence from the mock, not an oversight.
- The mark files themselves (`logo.svg`, `logo-compact.svg`, `favicon.*`) —
already drawn, already converted to outlines.
## Constraint that shapes everything
The panel's Content-Security-Policy is a plain `default-src 'self'` with no
inline-style exemption ([security.md](../security.md)). Every rule lives in
`panel.css`; a `style="..."` attribute in a template is blocked and silently
does nothing. Self-hosted fonts are served from the panel's own origin and are
therefore already covered — no CSP change is needed, and none may be made.
## Typography
IBM Plex, self-hosted. The mark is Plex converted to outlines, so the panel
setting its own name in Segoe UI or Cantarell is the seam this whole item
exists to close.
| File | Covers | Size |
|---|---|---|
| `static/ibm-plex-sans.woff2` | variable, weights 100700, latin | 45.7 KB |
| `static/ibm-plex-mono-400.woff2` | mono regular, latin | 14.8 KB |
| `static/ibm-plex-mono-600.woff2` | mono semibold, latin | 15.7 KB |
76 KB in total, in a 20 MB binary. The variable file replaces what would
otherwise be five static weights and lets the scale below use 300 and 500
without paying per weight.
Monospace is the one the operator actually reads: DKIM records, `mail.log`
lines, application logins, socket paths, generated passwords. `ui-monospace`
resolves to Consolas, SF Mono or DejaVu Sans Mono depending on the operator's
machine, and those differ in advance width — the six-column send log wraps
differently for each. A shipped mono makes those tables one layout.
| Role | Family | Size | Weight |
|---|---|---|---|
| Body | sans | 15px / 1.5 | 400 |
| `h1` | sans | 1.55rem, tracking 0.01em | 300 |
| `h2` | sans | 1.05rem | 600 |
| `label` | sans | 0.9rem | 600 |
| Nav entry / active | sans | 0.95rem | 400 / 600 |
| `th` | **mono**, uppercase, tracking 0.08em | 0.75rem | 500 |
| `.st` status badge | **mono** | 0.78rem | 500 |
| `.code`, `.mono`, `.metric` | **mono** | 0.85rem | 400 |
`font-display: swap`, so a cold load shows the system stack for a frame rather
than blank text.
## Colour tokens
Names stay as they are wherever they already exist: the dark scheme reassigns
the same custom properties, which is why no rule in the file needs
`!important`. Warm neutrals replace the cool greys; brick becomes the accent.
| Token | Light | Dark |
|---|---|---|
| `--bg` | `#F4F2ED` | `#16181B` |
| `--fg` | `#12161C` | `#E9E6E0` |
| `--muted` | `#6B7684` | `#9AA1A9` |
| `--card-bg` | `#FFFFFF` | `#1D2024` |
| `--border` | `#DEDCD7` | `#2C2F34` |
| `--control-border` | `#CBC8C1` | `#3A3E44` |
| `--input-bg` | `#FFFFFF` | `#14161A` |
| `--code-bg` | `#EFEDE9` | `#14161A` |
| `--surface-bg` | `#EAE7E0` | `#23262B` |
| `--accent-fill` / `--on-accent` | `#7A3B2E` / `#FFFFFF` | `#8E4535` / `#FFFFFF` |
| `--accent-text` | `#7A3B2E` | `#CE7B66` |
| `--nav-active-bg` | `#EDE4DE` | `#2A1F1B` |
Brick splits into a fill and a text value because `#7A3B2E` on `#16181B` is
about 2:1 — unreadable as a dark-scheme link. The fill lightens just enough to
keep white on it above 4.5:1; the text value lightens further.
Status families (`--st-ok-*`, `--st-warn-*`, `--st-error-*`, `--st-unknown-*`),
the flash, the credential card and `--danger-*` keep their hues and are only
warmed to sit on paper. The one thing to watch is brick against `st-error` red:
the proof rejected several candidate colours precisely so that the mark would
not read as a status, and the same test now applies to every brick button
standing in a row of `error` badges.
## Components
Everything already in `panel.css`, in the order it appears there: card, form
controls, buttons (filled, outlined, danger), flash, table, status badge,
`.code`, nav (brand, links, sections, session), application list and its
disclosure panels, credential card, status page meters and facts, delivery
timeline, log tables, split layout, encrypt fields, footer.
Two component-level changes rather than pure repaints, both forced by the
accent:
- Row actions (`td.actions a.danger`, `Delete`) become outlined instead of
filled. A filled red button in a table row next to a filled brick button
reads as one block of colour.
- Nav entries carry the active state as brick text on a warm tint rather than
the current blue tint.
## Order of work
1. Fonts into `static/`, `@font-face` and the type scale in `panel.css`,
`NOTICE` attribution. Nothing else changes shape.
2. Token block: light and dark, both schemes in one pass.
3. Chrome: `layout.html`'s nav, footer, shell.
4. Signed-out pages: `login`, `setup` — the mark and one card, where the seam
is worst.
5. `dashboard`, `domain_detail`, `domain_delete`.
6. `deliveries`, `deliveries_rows`, `delivery`.
7. `status` + `status_body`, `mail_queue*`, `system_log*`.
8. `backup`, `account`, `users`, `user_form`, `encrypt_fields`.
9. `CHANGELOG.md` under `[Unreleased]`.
## Verification
- Every page rendered locally and screenshotted in both schemes before and
after (`panel.exe` on Windows, headless Edge), including a 375px-wide pass —
the nav column and the wide tables are where a repaint breaks layout.
- Contrast: body text and every status badge at 4.5:1 or better against its own
background, UI borders at 3:1. Brick on white is 7.3:1 by the proof's own
measurement; the dark-scheme values above are the ones to re-check.
- `go build ./... && go vet ./... && go test ./...`; the template guards in
`internal/web/view/templates_test.go` must stay green, and the static-asset
ETag test grows to cover the three font files.
- No `style=` attribute anywhere in `templates/` — the CSP would drop it.
## What is done
The restyle itself landed in `652f1fe`, with the table-wrapping fixes it
surfaced in `f44f533`. Every page in the order above was rendered in both
schemes from a panel running locally and checked: the signed-out pair, the
domain list empty and with three domains, the whole domain page (credential
card, DKIM/SPF/DMARC, DNS status, applications, rate limit, export, danger
zone), the delete confirmation, the send log with rows, a single delivery,
status, mail queue, system log, backup, settings, users and the user form.
Two of those need standing in for what the container provides: `supervisorctl`,
`saslpasswd2` and `postmap` stubs on `PATH`, `postfix/`, `opendkim/keys/` and
`sasl/` created inside the data dir by hand, and rows seeded into `send_log`
without them the domain, application and send-log pages do not exist locally.
## Outstanding
Nothing here blocks the item; each is written down so it is not rediscovered.
1. **Send-log status is bare text**, while every other status in the panel is a
`.st` badge. Making it one is not a repaint: it needs a mapping from
`sent`/`queued`/`deferred`/`bounced` onto the four badge colours, which is a
judgement about severity (is `deferred` a warning?) rather than a style.
**Needs a decision before it is written.**
2. **The panel overflows horizontally at 375px**`main` and its cards render
wider than the window and the page scrolls sideways. Reproduced with the
stylesheet at `f59befd` too, so it predates this work; tracked separately.
Removing the navigation does not fix it, so it is in `main`/`.card`, not in
the bar the `@media (max-width: 66rem)` block lies down.
3. **Three views were only ever seen empty**: the mail queue with entries, the
system log with lines, and a delivery's own `mail.log` lines (`table.log`).
All three need a running Postfix, so they are a test-server check, not a
local one. `table.log` is the only restyled component with no screenshot
behind it.
4. ~~**CSP and the font ETags**~~**done** on the test server at
`1.1.0-post.669f928`. The policy is unchanged
(`default-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self';
frame-ancestors 'none'`) and admits all three fonts, which come back as
`font/woff2` with `Cache-Control: no-cache` and a content ETag: a matching
`If-None-Match` gets 304, a stale one gets the bytes. The signed-out page
renders in Plex over the network.
5. **`font-display: swap` has never been observed** — every render had the fonts
already on disk. Worth one cold load over the network to see how long the
system stack is on screen.
## Done when
- The panel and the mark read as one design in both schemes, at the reading
measure and on the wide data pages.
- Nothing an operator does changed: same pages, same controls, same copy.
- Fonts are served from the panel's own origin under the unchanged CSP, and the
image works with no network access.
- `NOTICE` credits IBM Plex (OFL-1.1); build, vet and tests are green.
- The three views in **Outstanding** 3 are seen with real data on the test
server, and 4 is confirmed there.
## Risks
- **Visual regression across 21 templates.** The panel has pages that are only
reachable mid-workflow (the credential card, the delete confirmation, the
encrypt fields). Mitigation: the page order above is a checklist, and each
step is screenshotted rather than assumed.
- **Brick against the error red.** If the two fight in a real row of the send
log, the accent gets pulled back to the mark and the buttons stay neutral —
the mark's colour is fixed, the panel's accent is the negotiable one.
- **Thin weights on dark.** `h1` at 300 is the one place a variable font makes
it easy to go too light; check it on the dark scheme before keeping it.
-70
View File
@@ -1,70 +0,0 @@
# Plan: web-split (splitting `internal/web`)
**Status:** done (see [CHANGELOG](../CHANGELOG.md) `[1.1.0]`)
**Version:** `1.x`; an internal refactor, it does not force a break on its own.
---
## What this is
`internal/web` is the project's largest package: ~50 files (templates and
static assets included), ~25 `.go` files and ~4000 lines of Go, with the
handlers for every panel section, sessions, security headers, origin checking,
form validation and template rendering all sitting in one flat namespace.
The candidates to split out are `web/handlers` and `web/auth`, or a cut along
the panel's own domains.
## Why now
At its current size the flat package reads fine: the file names
(`handlers_domains.go`, `handlers_apps.go`, `handlers_monitor.go`) do the work
directories would, and splitting would force exporting what is package-private
today — widening the internal API for cosmetics.
It starts to pay off once the package grows: **domain-admin** and
**inbound-relay** both add code to it — the role brings authorisation into
every handler, the inbound relay brings its own pages and handlers for inbound
domains. The refactor is cheaper before that growth than after it.
## Recommended order
**web-split → domain-admin → inbound-relay** (see the
[roadmap](../roadmap.md)).
1. **web-split** — lay down the package structure (including a place for
`web/auth`) while there are no cross-cutting edits from the role and no new
inbound handlers.
2. **domain-admin** — authorisation in every handler builds on a package layout
already chosen.
3. **inbound-relay** — a new vertical slice; easier to add to an already split
package than to refactor alongside the two features before it.
The order is a recommendation, not a blocker.
## Chosen scheme
**Horizontal split into four packages** (decided at implementation):
```
internal/web/ # Config, Server, New, Handler — composition root; security.go
internal/web/view/ # embed templates/static, render/renderFragment, staticHandler
internal/web/auth/ # session, login/logout/setup, requireAuth, currentUser
internal/web/validate/ # shared form validation (avoids auth ↔ handlers import cycle)
internal/web/handlers/ # all authenticated page handlers (handlers_*.go)
```
`cmd/panel` keeps importing only `internal/web`. Subpackages are not exported
beyond what the composition root needs.
## Done when
The package is split along the scheme above. After the split: `build`/`vet`/`test`
green, the panel's behaviour unchanged.
## Risks
- Splitting too early — a superfluous internal API and churn with nothing to
show for it;
- leaving it until after the growth — a harder refactor, tangled up with the
features.
+12 -9
View File
@@ -47,17 +47,19 @@ Explicitly excluded to prevent scope creep:
- Inbound mail (IMAP/POP3, mailboxes, delivery to user inboxes)
- Webmail
- Multi-user panel / organisations / roles — one administrator; managing
**multiple sending domains** is in scope (see below)
- Organisations / tenancy beyond global + domain-admin roles; managing
**multiple sending domains** under one global administrator is in scope (see
below)
- Inbound antispam/antivirus (rspamd, ClamAV, etc.)
- A custom MTA — Postfix is used as-is
- Dovecot or a full mail stack for SASL — Cyrus SASL (`sasldb2`) only
Agreed **1.x+** extensions (optional inbound relay, domain-admin role) are
tracked in [roadmap.md](roadmap.md) and [plans/](plans/). Inbound relay targets
a 1.x MINOR bump by default; a 2.x major remains possible pending
implementation. Items still marked *candidate* in the roadmap require explicit
approval before coding.
The **domain-admin** role ships in the current line (global administrator plus
domain administrators with assigned domains). The optional **inbound relay** is
the main agreed **1.x+** extension still on the
[roadmap](roadmap.md) — it targets a 1.x MINOR bump by default; a 2.x major
remains possible pending implementation. Items marked *candidate* in the
roadmap require explicit approval before coding.
---
@@ -102,5 +104,6 @@ Adding a domain does **not** create an application automatically.
- **Delete domain** — removes DKIM key and **all** its applications.
- **Delete application** — removes only that app's SASL and map entries.
This is not multi-tenancy (one admin); it is one owner operating several
sending domains with independent application credentials.
This is not multi-tenancy; it is one owner (or a small team with global and
domain-scoped roles) operating several sending domains with independent
application credentials.
+3 -52
View File
@@ -26,16 +26,14 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| ID | Topic | Status | Plan |
|---|---|---|---|
| domain-admin | Domain administrator role | **agreed** | [plans/domain-admin.md](plans/domain-admin.md) |
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | [plans/inbound-relay.md](plans/inbound-relay.md) |
| contributing | `CONTRIBUTING.md` | candidate | — |
| visual-style | Panel visual style | **agreed** | [plans/visual-style.md](plans/visual-style.md) |
| dmarc-reports | DMARC aggregate report ingestion and panel UI | candidate | [plans/dmarc-reports.md](plans/dmarc-reports.md) |
| logrotate-mode | `mail.log` stops rotating in some builds | candidate | [plans/logrotate-mode.md](plans/logrotate-mode.md) |
**Recommended order** (not binding): **domain-admin →
inbound-relay** — role-wide authorisation first, then the inbound relay vertical
slice. Deviating is allowed; there are no hard phases here.
**Recommended order** (not binding): **inbound-relay** first among agreed
items — it is the largest remaining 1.x+ extension. Candidates need explicit
agreement before they join the queue.
After a context reset, pick an item marked `agreed` or `in progress`, then work
the checklist in its linked plan.
@@ -58,33 +56,11 @@ engine stays outside the image, only the attachment point is provided.
**Dependencies / risks:** a finished outbound path; open relay and backscatter;
a wider attack surface (port 25 accepting mail).
**Order:** recommended after [domain-admin](plans/domain-admin.md).
**Version:** target bump `1.x`; `2.x` possible — to be settled once the
implementation lands.
---
## domain-admin
**Goal:** a role with access to one or several assigned domains (the list is
set by the global administrator) — applications, DKIM/DNS, and the send log for
each of them; without global operations (adding domains, full backup, the
queue, `mail.log`).
**Boundary:** an extension of v1.0 — [product.md](product.md) fixes a single
administrator. Not a second all-powerful admin, but limited access to the
assigned domains (one or several).
**Done when:** see [plans/domain-admin.md](plans/domain-admin.md).
**Dependencies / risks:** a users table, the role in the session, authorisation
in every handler, setup and backup. **Order:** recommended **before**
[inbound-relay](plans/inbound-relay.md).
**Version:** `1.x` MINOR, given a compatible migration of the current
administrator into a global one.
---
## contributing
**Goal:** `CONTRIBUTING.md` in the root — the dev loop, the checks to run
@@ -103,30 +79,6 @@ priority.
---
## visual-style
**Goal:** refresh the control panel's visual design — typography, colour tokens,
spacing, and component styling — without changing operator workflows or panel
behaviour.
**Boundary:** presentation only (`panel.css`, templates, static assets); no new
features. Styling must stay compatible with the panel CSP — rules live in
`panel.css`, not inline (see [security.md](security.md) and the stylesheet
header).
**Done when:** see [plans/visual-style.md](plans/visual-style.md). The agreed
direction is the mark's own — brick, warm paper, IBM Plex — taken from
[assets/selfpost-proof.html](assets/selfpost-proof.html); light and dark schemes
remain supported; readability and contrast are preserved.
**Dependencies / risks:** CSP constraints on how styles are applied (rules in
`panel.css`, never inline); visual regression across pages; the accent colour
must not read as a status badge.
**Order:** independent of the feature items; may be taken up between them.
**Version:** no bearing on semver.
---
## dmarc-reports
**Goal:** SelfPost receives DMARC aggregate reports (RFC 7489) on SMTP,
@@ -150,4 +102,3 @@ source of truth for `rua=` in DNS guidance.
inbound-relay depending on how port 25 acceptance is structured.
**Version:** `1.x` MINOR.