Comments only, no behaviour changed. modernc.org/sqlite v1.60.1 (the
_pragma list on every connection, busy_timeout first; extended result
codes; a deferred BEGIN; multi-statement Exec) and SQLite 3.53.4's
documentation (transactions, WAL, foreign keys, NOCASE, upsert, julianday,
partial indexes); htmx 2.0.4 src/htmx.js; the Fetch and HTML standards for
Sec-Fetch-Site, Origin, SameSite, __Host-, CSP and defer order; Go's
html/template. Comments the source contradicts are corrected - the driver
does not serialise writes (SetMaxOpenConns(1) does), htmx:load fires per
swapped element, no-store does not keep a page out of history.
Left for their own commits: D-S3 a migration two processes can both run,
D-S20 recipient case, D-B3 Origin: null under Referrer-Policy: no-referrer,
D-H5/H6/H7/H9 (audit M11, stage 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 6/12 of docs/plans/inbound-antispam-panel.md. Written by a Sonnet
subagent from the mockups, reviewed and corrected by me.
GET /inbound/log - the journal a page at a time (50), filtered by
domain, decision and day; QueryInboundSpam / CountInboundSpam with the
viewer's inbound domains as the scope. The global role reads all of it
and gets "Deny sender" / "Allow sender" links into the lists; a user
with the domain role reads the rows of the inbound domains assigned to
them, without the links. The plan said global-only for the journal;
the accepted mockup and 2.0's delegation of Inbound say otherwise -
recorded in the plan as built.
GET, POST /inbound/filter-lists and POST .../{id}/delete - global role,
404 otherwise. Add form (prefilled from the query), the two lists with
Remove as the kit's row action, and "Where the lists go" with the
filter's state and when the maps were last written
(Lists.LastWritten). A refused or duplicate entry is 422 with the
message and what was typed.
Server > Settings: "Keep inbound log rows, days" with the relay on.
Help: Inbound log, Filter lists. Navigation: Log for everyone who sees
Inbound, Filter lists for the global role (Shell.ShowFilterLists).
Status tags accept / reject / tempfail join the kit's mapping.
Tests: page fixtures for both screens, held to outlines.json; view and
handler tests (both roles, filters, paging, relay off, add / duplicate
/ invalid / delete, map file content, settings field, help). e2e reads
the journal from the page instead of the database and adds and removes
a deny entry through the panel, reading deny_domain.map after each.
Seen: both pages and Settings rendered from their fixtures and compared
with the mockups side by side (headless Edge, 1360 px) - same layout
and wording.
Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the design: commit before it. panel.css gains the same four rules
theme.css did (TestPanelCSSIsTheMockupStylesheet holds them equal): the form
stretches the button across the user menu and the button is handed Bulma's
hover / focus / active variables. layout.html already had the form; only its
comment changes.
Checked in the browser on the kit page at 1440: Sign out is as wide as
Account and Help (147 px, 36 px tall), shows the pointer cursor, and is
tinted under the pointer like they are.
go test ./internal/web/...
ok github.com/mixeme/selfpost/internal/web
ok github.com/mixeme/selfpost/internal/web/auth
ok github.com/mixeme/selfpost/internal/web/handlers
ok github.com/mixeme/selfpost/internal/web/validate
ok github.com/mixeme/selfpost/internal/web/view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 0, steps 3-5, in one commit: the three were built together (the kit
page is what exercises the partials and the shell) and do not split into
states that build on their own. Written by Sonnet, reviewed by Opus.
- static/panel.css: shared/brand.css + panel/theme.css, rule for rule.
wordmark.svg and perf-edge.svg beside it; the stamp is logo.svg.
- templates/components.html: every partial of the plan's Three layers,
with its typed input in components.go. Wrappers are open/close pairs
(box_open/box_close, body_open/body_close, record_open/record_close).
Rich text is a Text value built from escaped runs - no field is
trusted HTML. status_tag and wbr_at are template functions; copy_field
is a partial whose button panel.js wires through data-copy.
- templates/layout.html: the shell as build.py draws it, the menu paths
written out literally, visibility from a typed Shell (role, feature
flags, the user's reach); the signed-out screen as a second define.
Not in the 2.0 menu: Inbound Log / Queue / Quarantine / Filter lists
and Preflight.
- GET /server/components (global role, 404 otherwise): the mockup
components page rebuilt from fixtures (kit.go) by the real templates.
- The old panel steps aside unchanged: panel.css -> legacy.css,
layout.html -> layout_legacy.html; a page uses the new layout iff it
is off legacy_pages.txt.
- legacy_pages.txt: @kit removed - the kit is under the guards for good.
Differences from the mockup components page, for the owner:
- the head button is Help, not "Cover sheet"; two "Example" links are
dropped (no fixed URL), two point at /outbound/domains and /login;
- the page sits in the Server group, so the sibling strip shows;
- Sign out is a POST form whose button carries navbar-item: same size
and type as the links, but without Bulma's hover tint;
- a head's actions are always wrapped in div.buttons.
Evidence
- docs/assets/panel-redesign/evidence/components.png: 1440 px wide, full
height, headless Edge. Taken from the real templates served locally,
NOT from the container (no Docker on this machine); the page reads
only its own fixtures and embedded assets.
- panel/audit.js on the kit page, 1440 and 1280:
{"pages":1,"widths":[1440,1280],"textsChecked":516,"problems":[]}
- go test ./internal/web/... (with @kit removed)
ok github.com/mixeme/selfpost/internal/web
ok github.com/mixeme/selfpost/internal/web/auth
ok github.com/mixeme/selfpost/internal/web/handlers
ok github.com/mixeme/selfpost/internal/web/validate
ok github.com/mixeme/selfpost/internal/web/view
TestRoutesFollowNavigation stays skipped behind @routes (stage 1).
Roadmap 10/27.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Built-in operator documentation in the panel: a CSS-only help drawer and Help page so Status checks and domain controls are explained without leaving the panel.
Added:
- **Help** navigation entry and `/help` index page.
- CSS-checkbox help drawer (no extra script): seeded topics for Status (Machine,
- «?» entry points on Status and domain card heads open the matching drawer
Changed:
- [guide.md](docs/guide.md) documents the boundary: in-panel help for card
Full detail: CHANGELOG.md (1.8.0).
Optional DMARC aggregate report ingest: accept `rua=` mail on port 25, parse gzip/XML into SQLite, and show summaries in the panel.
Added:
- Optional DMARC aggregate report ingest (`DMARC_REPORTS_ENABLE`): port 25
- Per-domain **SelfPost hosted** `rua=` mode when ingest is enabled
Full detail: CHANGELOG.md (1.7.0).
Optional inbound relay (backup-MX / forwarder) on port 25, off by default. Mail is accepted only for domains configured in the panel and forwarded to an upstream; there are still no mailboxes. A dedicated security review of the inbound path is still open on the plan (Fable).
Added:
- `INBOUND_RELAY_ENABLE` (default `false`): when true, Postfix listens on
- Panel **Inbound** pages (global administrator): domains, upstream host/port
- Optional `INBOUND_ANTISPAM_MILTER` on the inbound listener only, plus
- Inbound rate limit (`INBOUND_RATE_LIMIT_MESSAGES_PER_IP`, default 20) and
Changed:
- Compose publishes host port 25 even when the flag is off (nothing listens
- Full-backup restore Resync also rebuilds inbound maps when the flag is on.
Full detail: CHANGELOG.md (1.4.0).
[1.2.0]
The second MINOR after 1.0.0: domain administrators with per-domain scope, a panel visual refresh on the SelfPost palette, and refinements to navigation and the send log. Upgrading runs one SQLite migration (`0005_panel_users`); the single administrator becomes a global user.
Added:
- panel: **domain-admin role** — global administrators manage panel users and
Fixed:
- Docker build: `LICENSE` is no longer excluded by `.dockerignore`, so the
- panel: on signed-in pages with a narrow card (**Settings**, the user form)
Changed:
- panel: navigation session icons are distinct — **Settings** uses a gear,
- panel: `/` redirects domain administrators to `/domains`; global users still
- panel: **Sign out** in the navigation column uses the same type size and
- panel: **visual style** brought in line with the SelfPost mark — brick accent
- panel: the send log's **status is a badge**, in the same ok/warn/error/unknown
… (1 more in CHANGELOG)
[1.2.1]
Panel refinements after 1.2.0: navigation icons, status-badge centreing, drill-down back-link placement, and safeguards for the sole global administrator. Upgrading is a tag bump; no migration.
Fixed:
- panel: status badge text sat low in the box (and below the heading or label
- panel: the **Users** navigation icon was two full silhouettes with staggered
- panel: the **Settings** navigation icon was a sun-with-rays (circle plus
- panel: the user create/edit form placed «Back to users» at the bottom of the
Changed:
- panel: the user edit form disables role change and delete for the only global
- panel: the user create/edit form hides **Assigned domains** when the role is
- panel: **Settings** shows panel credentials and DMARC aggregate reports side
[1.2.2]
Status page layout after 1.2.1: paired cards in a wide column, denser machine and check copy, and a user-form checkbox fix. Upgrading is a tag bump; no migration.
Changed:
- panel: **Status** is wide again so paired cards fill the column. Layout:
Fixed:
- panel: cards inside `.split` used `margin: 0 auto`, which in a CSS grid
- panel: on the user create/edit form, **Assigned domains** checkboxes stacked
[1.2.3]
Domain detail layout and panel polish after 1.2.2. Upgrading is a tag bump; no migration.
Changed:
- panel: the domain detail page is wide with paired cards (DKIM+SPF ‖ DMARC;
- panel: Domains list — **Add domain** sits beside the domain field; the
- panel: page URLs, browser titles, and headings are aligned — **Settings** is
Fixed:
- image: `mail.log` rotation no longer silently stops when the build context
[1.2.4]
Level-2 rate-limit semantics inverted after 1.2.3, plus a small DNS field height fix. Upgrading is a tag bump; no migration.
Changed:
- rate limiting (level 2): domain ceilings apply to every client IP (no IP
Fixed:
- panel: on the domain DNS status grid, the Type (TXT) field height matches
[1.2.5]
Rate-limit form polish after 1.2.4. Upgrading is a tag bump; no migration.
Changed:
- panel: rate-limit forms refined — level-1 backstop as muted copy on the
Squashed CHANGELOG sections: [1.2.0], [1.2.1], [1.2.2], [1.2.3], [1.2.4], [1.2.5].
Full detail: CHANGELOG.md (1.2.0, 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5).
The first MINOR after 1.0.0: send-only DMARC guidance in the panel, AGPL packaging on every page, and an internal split of `internal/web` ahead of domain-admin work. Upgrading runs one SQLite migration (empty defaults; existing DNS guidance is unchanged until you set a report address).
Changed:
- `internal/web` split into subpackages (`web/view`, `web/auth`, `web/validate`,
Added:
- panel: DMARC guidance for send-only relays — the suggested `_dmarc` record
- AGPL packaging hygiene: [NOTICE](NOTICE) names the copyright holder and the
- [docs/roadmap.md](docs/roadmap.md) — candidate item **visual-style** (panel
Full detail: CHANGELOG.md (1.1.0).