From 5eaf665c01ebbc913ec3217e07add53f1b9eca0b Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Tue, 11 Aug 2026 01:34:04 +0300 Subject: [PATCH] docs: remove completed plans and trim the open-work tracker Co-authored-by: Cursor --- CHANGELOG.md | 5 + docs/plans/domain-admin.md | 259 ------------------------------------ docs/plans/inbound-relay.md | 5 +- docs/plans/visual-style.md | 226 ------------------------------- docs/plans/web-split.md | 70 ---------- docs/product.md | 21 +-- docs/roadmap.md | 55 +------- 7 files changed, 23 insertions(+), 618 deletions(-) delete mode 100644 docs/plans/domain-admin.md delete mode 100644 docs/plans/visual-style.md delete mode 100644 docs/plans/web-split.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f344c51..0a493bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/plans/domain-admin.md b/docs/plans/domain-admin.md deleted file mode 100644 index 3e62e87..0000000 --- a/docs/plans/domain-admin.md +++ /dev/null @@ -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. diff --git a/docs/plans/inbound-relay.md b/docs/plans/inbound-relay.md index e27311d..a9e4e63 100644 --- a/docs/plans/inbound-relay.md +++ b/docs/plans/inbound-relay.md @@ -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]`). --- diff --git a/docs/plans/visual-style.md b/docs/plans/visual-style.md deleted file mode 100644 index 420a363..0000000 --- a/docs/plans/visual-style.md +++ /dev/null @@ -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 100–700, 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. diff --git a/docs/plans/web-split.md b/docs/plans/web-split.md deleted file mode 100644 index 97c7b00..0000000 --- a/docs/plans/web-split.md +++ /dev/null @@ -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. diff --git a/docs/product.md b/docs/product.md index 823e445..95ce410 100644 --- a/docs/product.md +++ b/docs/product.md @@ -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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 84a7063..3641ed0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. -