- .flash.error now renders on the danger surface instead of the success
one; RateLimitErr previously showed as green with red text.
- User delete goes through a confirmation page (GET/POST
/users/{uid}/delete), matching the domain-delete pattern, instead of a
plain submit button next to Save with no confirmation.
- Extracted the repeated DNS Host/Type/Value markup on a domain's page and
the duplicated Settings credentials form into shared partials. No
behaviour change.
docs/plans/code-review.md P3 checked off; CHANGELOG updated.
Co-authored-by: Cursor <cursoragent@cursor.com>
The CSRF ADR, guide.md, architecture.md route table, the internal/backup
package comment, roadmap/dmarc-reports admin.dmarc_report_email references,
the e2e coverage summary, and the setupManager/store package comments still
described the pre-1.2.0 single-administrator panel or stale implementation
details. Closes P2 of docs/plans/code-review.md; no behaviour change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The level-2 limiter counted stored plus in-flight messages and reserved its own slot in two critical sections, so SMTP sessions that overlapped could each take the last free slot; tryAdmit now does both under one lock. A session that cannot be written no longer yields a cookie the browser would carry while every request bounced to /login. Deleting an application clears its SASL account before its registry row, matching domain delete, so a saslpasswd2 failure leaves a retryable application rather than an account that still authenticates.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The release gate timed out waiting for status=sent because its HTML scrapers had drifted from the panel's badge markup; record the fix and the handler regression test.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Deliveries list narrowed the journal only when exactly one domain was assigned, so an administrator with none or with two or more read every domain's rows. The domain scope is now an IN constraint the store query carries, a filter that states no scope matches nothing, and the domain/app query parameters are validated against the principal before the query runs.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture the 2026-08-13 review as the active plan, queue it ahead of inbound-relay, and defer SQLite migration squash to 2.x with a user_version gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the shared-baseline flex pin on .split cards; mismatched body
lengths made Export / Danger look worse than the plain stack.
Co-authored-by: Cursor <cursoragent@cursor.com>
Subgrid Domain settings rows, match Address mode and trusted-IP controls,
and pin Export/Danger buttons to a shared baseline in split cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the app L1 backstop line, move trusted-IP help under the IP field,
shorten the domain rate-limit title, and align field-pair labels with DMARC.
Co-authored-by: Cursor <cursoragent@cursor.com>
Restore muted L1 backstop line, move domain limit help above Save, and drop
the duplicate Address mode field label.
Co-authored-by: Cursor <cursoragent@cursor.com>
1.2.4 stays as cut at the release commit; the L1 display and badge work
belongs in Unreleased until the next version cut (development.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
Show the level-1 backstop as its own line and code-row, use st badges for
active/inactive domain and app limits, and fold the polish into 1.2.4.
Co-authored-by: Cursor <cursoragent@cursor.com>
Close Unreleased for the level-2 rate-limit semantics change and DNS Type
field height fix; pin compose and docs to 1.2.4.
Co-authored-by: Cursor <cursoragent@cursor.com>
Invert level-2 semantics so domain limits apply to every client IP and
application limits with trusted IPs raise the ceiling above the domain
(still capped by level 1). Panel shows L1, validates maxima, and documents
the model on Settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
Close CHANGELOG [Unreleased] as 1.2.3; bump the compose pin and image references. Includes domain detail layout polish and Domains add-row.
Co-authored-by: Cursor <cursoragent@cursor.com>
Settings moves to /settings with a 308 redirect from /account; domains, Status, Users, and user forms get matching browser titles and h1 text; backup page title reflects domain import.
Co-Authored-By: Composer <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Close CHANGELOG [Unreleased] as 1.2.2; bump the compose pin and image references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Pair Machine|Processes, queue|cert, and sockets|hostname; fix .split
auto-margins so cards fill half the row; trim Status prose and machine
details; drop the page section index; note panel-docs on the roadmap.
Co-authored-by: Cursor <cursoragent@cursor.com>
Widen the status page and wrap the short queue/certificate cards in
.split so the stack is shorter; other checks stay full width.
Co-authored-by: Cursor <cursoragent@cursor.com>
Form-wide block labels and full-width inputs stacked the box above the
domain name; checkbox rows now share label.check and fieldset spacing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Close CHANGELOG [Unreleased] as 1.2.1; bump the compose pin and image references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
IBM Plex Mono sits low in its em square; top-heavy badge padding left
ok/warn sunk below the adjacent heading. Bottom padding is now heavier.
Co-authored-by: Cursor <cursoragent@cursor.com>
Global administrators manage every domain; the assignment checkboxes are
now hidden when that role is selected.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two full silhouettes with staggered baselines read lopsided at 16 px; the
rear person is now a right-side crescent behind an aligned front silhouette.
Co-authored-by: Cursor <cursoragent@cursor.com>
The icon was a sun-with-rays (circle plus spokes), not a cog, despite the
1.2.0 note that Settings used a gear.
Co-Authored-By: Cursor <cursoragent@cursor.com>
The user form had Back to users at the bottom of the card; all drill-down
pages now use a shared back_link template with a structural test.
Co-Authored-By: Claude <Composer 2.5> <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Global administrators get two cards in the .split layout; domain-scoped
users keep the single narrow form.
Co-Authored-By: Claude <Composer 2.5> <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Close CHANGELOG [Unreleased] as 1.2.0; correct missing entries; bump the
compose pin and image references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Settings and the user form centred the title on the 48rem measure while
.card.narrow centred on 24rem, so their left edges drifted by 12rem. Cap every
direct child of main when a narrow card is present, without shrinking the
column, so navigation stays put and login/setup stay unchanged.
Co-Authored-By: Cursor <cursoragent@cursor.com>
Settings had been sharing the account silhouette with Users and the signed-in user label, so three different concepts read as the same icon. Settings now uses a gear, the user line the single-user icon, and Users a two-person group mark.
Co-Authored-By: Cursor <cursoragent@cursor.com>
The send log was the one place in the panel where a status was bare text, next
to a delivery's own page where the same value is a badge, and a domain list
where the DNS verdict is one too — so a message's outcome looked like a
different kind of fact depending on which page you read it from.
The level travels on the row rather than being worked out in the template:
deliveryLevel is the one place that decides what a status means, the delivery
page already reads it, and a second mapping written into the template or the
stylesheet would have been free to drift from it. sent is ok, deferred a
warning, bounced and rejected errors, and queued unknown — nothing has gone
wrong, nothing has been reported yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last cell in a row is the one the automatic table layout squeezes first,
and in the send log it holds a fixed two-word link: "Details" was being split
after "Detail" on every row. Holding that cell on one line costs the row
nothing and hands the width back to the status column, which was breaking
"deferred" into "deferre" and "d" for the same reason — the status gets the
same nowrap the timestamp already had, through a class of its own since the
cell had nothing to select on.
Both predate the restyle; they surfaced while screenshotting the send log
against a seeded journal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stamp was approved on its own sheet and the panel kept the blue-on-cool-grey
it shipped with, so the mark read as pasted onto someone else's page. Brick
becomes the accent, the greys warm to paper, and the corners square up towards
the stamp's own edge.
Brick is two tokens rather than one. #7A3B2E under white text is 8.4:1, but as
text on the dark scheme's background it is about 2:1 — a link nobody can read.
So --accent-fill carries controls and --accent-text carries links and the active
navigation entry; in the light scheme they coincide, in the dark one they do not.
IBM Plex ships with the panel instead of being named and hoped for. The mark is
Plex converted to outlines, so a system stack left the panel's own name as the
only Plex on the page — and ui-monospace resolves to Consolas, SF Mono or DejaVu
Sans Mono depending on the machine, which laid the send log and mail.log tables
out differently for every operator. Three WOFF2 files, 76 KB, served from the
panel's own origin so default-src 'self' still covers them.
Two things changed shape rather than colour. Links had no rule at all and ran on
the browser's blue, invisible while the accent was blue and wrong the moment it
was not. And the column-narrowing that login and setup had by name now keys off
the narrow card itself, so Settings and the user form stop splitting their
heading from the card it belongs to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Close CHANGELOG [Unreleased] as 1.1.0; remove erroneous DMARC entries
retroactively copied into older sections; bump the compose pin and image
references.
Co-Authored-By: Cursor <cursoragent@cursor.com>
Lay out view, auth, validate, and handlers under internal/web while keeping
the cmd/panel API unchanged; update roadmap and changelog for web-split closure.
Co-authored-by: Cursor <cursoragent@cursor.com>
The panel now suggests policy-only DMARC by default, lets operators configure a default and per-domain report address, and DNS-checks hub _report._dmarc records. Future in-panel report ingestion is tracked as dmarc-reports in the roadmap.
Co-authored-by: Cursor <cursoragent@cursor.com>
Name the copyright holder, ship NOTICE with the image, serve the licence
from the panel footer on every page, and record the vendored htmx licence.
Co-authored-by: Cursor <cursoragent@cursor.com>
Close the [Unreleased] section as 1.1.0 and move the deploy pin, the README
trial command, and the guide's stated pin to that tag.
Corrections to the section while cutting it:
- The GHCR "unknown blob" retry was filed under Unreleased, but the commit
carrying it is what v1.0.0 points at. Moved into the 1.0.0 section.
- docs/plans/ and the roadmap's restructure into a 1.x+ tracker were missing
entirely; the plans were described only as "translated", which reads as if
they predated this release. Filed under Added.
- docs/product.md's reframing of the future line -- agreed 1.x+ extensions,
inbound relay targeting a MINOR by default, approval needed only for
candidates -- was unrecorded.
- The Account/Settings rename also touched the operator guide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The question kept resurfacing while preparing the repository for publication,
so it is written down as settled rather than re-argued: SelfPost is written by
AI agents under a maintainer's direction and the project says so.
What that commits to is listed explicitly — the Co-Authored-By trailers, the
model routing table, the .cursor rules file, and the agent-facing process notes
in the roadmap all stay, and none of them is an oversight to be tidied away.
The reasoning is that with the trailers already in ~140 commits, removing the
surrounding material would not conceal authorship, only signal an attempt at
it.
The revisit condition is deliberately narrow: a conflict with the licence or a
downstream obligation, not a shift in convention.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>