Agreed with the owner on 2026-10-13 (the panel in a chosen zone, "today"
that zone's day, the UTC labels gone) and settled on 2026-10-16/17: the
container runs in his zone ("Контейнер в моем поясе") - TZ in .env, read
by every program at its start - so the zone is shown on Settings, not
chosen there ("Да"), as the level-1 limit is.
Every mockup loses "UTC": the column heads are Time, Received, Held,
Arrived; a stamp is a date and a time. Settings gets box 05, Time zone,
read-only: the zone, its offset, where it is set, and that every time and
day of the panel is that zone's, as are mail.log's stamps. The help's
Settings topic says so. outlines.json: settings gains the box. check.py: 0
violations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Health's left stack is one polled element (health_stack, swapped whole
every refresh): a form there would lose the address being typed. Card 09
moves under Configuration, the right stack's static end, as Configuration
itself sits outside the poll for its button. On a phone the cards keep the
order of their numbers (data-order). outlines.json rewritten for health.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided by the owner on 2026-10-13 (out of preflight), taken up on
2026-10-16; the sender is the owner's choice of 2026-10-16 ("Да, адрес
годится"): postmaster@ the hostname, as notifications. Server › Health
gets card 09, Test message: one recipient and Send. The message goes as
notifications go - through Postfix, signed with the key of the server's
own name - and opens in the Outbound log, whose page follows it to the
receiving server; ten an hour. The help gets a Test message topic: what
the test proves (the server's path, not a domain's) and what it does not
(the inbox). outlines.json: health gains the box, help one topic.
check.py: 0 violations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner, 2026-10-16: "Может match ставить бэйджем в заголовок names?"
The certificate card loses its own "Matches the hostname" row: a tag
beside Names says it - "matches", or "not for the hostname" in red. A
fact's label may carry a tag (sp-facts dt .tag keeps the tag's own type),
shown on the kit page. Help says where the verdict is. check.py: 0
violations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner, 2026-10-16, on the mockup: "Может Greeting на всю ширину
карточки?" The facts grid gets sp-wide, a fact over the whole row (shown
on the kit page), and the greeting uses it. The greeting itself is what
this image's Postfix writes: "220 mail.example.org ESMTP"
(build/postfix-config.sh sets smtpd_banner to "$myhostname ESMTP"),
not Debian's longer default. The Configuration card's address sits in a
block of its own above the text. check.py: 0 violations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided by the owner on 2026-10-13 (in place of preflight), taken up on
2026-10-16 ("Делай все согласованные"). Three lines in Health's cards,
no new card: TLS certificate - "Matches the hostname"; Hostname and
reverse DNS - "Greeting", Postfix's 220 line; Configuration - "Your
address", the address the panel sees the browser at and where it took it
from. The help says when each fails and what to do. check.py: 0
violations; outlines unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner, 2026-10-16, of two ways for the navigation of a narrow screen
(pictures at 390 and 980 px): "Вариант 1. Но меню выглядит длинной
простынёй. Там бы сделать сворачивание разделов."
The cause, measured: the bar's menu was always open (navbar-menu
is-active, no burger), and Bulma lays it out below 1024 px as a sheet of
every section's pages, filling the first screen - on every phone, and on
a phone in "desktop site" mode (about 980 px) under columns that Bulma
puts side by side from 769 px; and a container has no side margin below
1024 px, so the page met the screen's edge.
Now, below 1024 px: the bar's menu is hidden (no is-active), and a Menu at
the right of the brand opens a panel over the page - Overview, then each
section as a fold, the current one open, the user's last (Account, Help,
Sign out). <details> all through: no script, like the folded DNS records.
The page and the strip of sibling pages get a 1rem gutter. From 1024 px
nothing changes. check.py: 0 violations; outlines unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Health: the opendkim process row goes; the Milter sockets card shows the
panel's signer on its two sockets, dkim-signer · applications (sign.sock)
and · notifications (sign-local.sock) - Q3 of the plan; Configuration
says it reloads Postfix. Domain delete: "stop signing mail as the
domain" for the OpenDKIM reload. System log: mail.log is Postfix's log
(OpenDKIM never wrote to it), its sample line goes. Message page: the
sample's OpenDKIM line becomes a qmgr line. Help, sender check: the
From: line is read as a mail program reads it and must be one address;
DMARC holds the signature to it; the sentence the mockup carried twice
is once. Overview: 5 of 5 sockets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since stage 4 the button also writes the spam filter's lists and the DMARC
maps (Postfix maps), and removes the keys of deleted domains; the box said
OpenDKIM and Postfix only (review of audit stage 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a phone the stylesheet ordered a cards box by its place in its stack,
four to a stack; a fifth box got no rule and landed out of place with
nothing failing (owner, 2026-10-13). Each box of the layout carries its
number as data-order, and theme.css orders by it, up to twelve: an
attribute, since check.py refuses classes the pages do not use, and not a
style, which the CSP forbids. Health and the kit's cards carry it; the look
is the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audit 2026-10-12, stage 2c, "Health says what is served" - the mockup
first.
TLS certificate (assumptions A-65): Postfix serves its own copy of the
mounted file, refreshed by cert-reload once a day by default, so the card
reads that copy. When the mounted file holds a renewal not taken up yet,
two facts name it (its expiry, when it was mounted) and the paragraph says
when Postfix takes it up; the help topic says the card warns once it has
waited longer than that check allows.
Wording (D-P1, D-P3): a message refused while the panel or the spam
filter does not answer is never queued, so it is not "deferred" in
Postfix's sense - the Sender check note, the sender-check and spam filter
help topics now say "refused with a temporary error". The upstream
topic's Required: mail waits in the queue while the upstream offers no
TLS or the handshake fails, and an upstream answering STARTTLS with a
permanent error bounces it (Postfix 3.7.11, src/smtp/smtp_proto.c;
smtp_trouble.c, smtp_bulk_fail()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner, 2026-10-14 ("Может просто показывать и то, и то и не смешивать?"):
the figure that was "Messages deferred" is two - connections Postfix turned
away while the panel did not answer (refused from the connection on, before
a message is offered), and messages deferred in the middle, when the panel
stopped answering after a message had begun (cleanup's case, counted since
the M13 fix). The card has a column for each.
The audit's stage 2b also marks a day whose downtime is an estimate - a
start at which supervisord could not say since when Postfix ran: such a
period's time is shown with ≈, and the text under the table says it is at
most what is shown.
Help: topic sender-check says what each column counts and what ≈ means;
topic log names the bounced row the panel closed itself (the sweep, when
Postfix no longer held the message and mail.log never said what became of
it). build.py, check.py: 0 violations, outlines unchanged. The code follows
in the next commits; until then the help text guard fails - not pushed in
between.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accepted by the owner on 2026-10-12 from screenshots, one change at a time.
- health: a card "Sender check" (today / 30 days / 12 months: panel not
answering, messages deferred) and a row for its socket.
- health: the boxes are numbered by importance and laid out as `cards` -
two stacks, odd numbers on the left and even on the right, each closed
up. His words on the row layout: too many holes; and on stacked columns
numbered down each side: the third box was below the fold while the
fourth was not. On a phone the boxes come in the order of their numbers.
- theme.css, components: the `cards` layout; the first pair of stacks of
the components page is laid out so.
- out-log: a row refused for its From: line, tagged "wrong From".
- help: topic "Sender check"; "rejected" in the Outbound log topic names
the three reasons.
outlines.json rewritten (check.py --write): the structure of health changed,
and help has one more topic under Outbound. check.py: 0 violations.
The pages of internal/web follow in their own commit; until then the
outline guard fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accepted by the owner on 2026-10-12 from screenshots, after two changes of
his: DKIM for the server's own name as well as SPF ("we need the best
deliverability for these messages"), and the records themselves under
Settings with only their state on Health.
- account: a checkbox "Send me notifications" in the Profile box, under the
e-mail it writes to. No box of its own, no choice per event.
- settings: box 03 Notifications with the instance switch; box 04
Notification mail with the two DNS records of the server's own name, DKIM
and SPF, drawn with the record component of a domain's DNS page. Sending
rate limits move to the left column as box 02.
- health: card 06 Notification mail - the state of each record and a link to
Settings - beside Configuration, now 07, each half the width.
- help: a Notifications topic (what is sent, when, to whom, what makes it
arrive); Settings and Your account each gain a clause.
outlines.json rewritten with check.py --write: the structure of settings and
health changed. build.py: 35 pages; check.py: 32 screens, 52 sp- classes,
0 violations. The plan records both of his changes; step 1 of 10.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's decision (2026-10-10): help is needed by almost every
section, not by Overview and Outbound alone. The Help mockup had six
topics while other screens already linked to topics it did not have
(#dmarc, #inbound).
- src/help.html: seventeen topics in five boxes - Overview (checks,
certificate, rdns), Outbound (dns, apps, limits, log, queue, dmarc),
Inbound (inbound, upstream, recipients, filter), Server (backup,
users, settings), Account (account) - one paragraph each, and the
side menu lists them by group.
- A box head gets the sp-help link where a topic now exists: health,
out-domain-settings, out-app, out-log, out-message, out-queue, the
three DMARC screens, in-domains, in-domain, backup, users, user,
settings. No link on boxes that explain themselves (confirmations,
the shown-once password, sign-in) and at most one per box.
- outlines.json regenerated (check.py --write): only help changed -
Outbound holds six topics, and Inbound, Server and Account are new
boxes. 0 violations.
The dns, dmarc and account topics describe the report-address scheme the
owner decided the same day (no reports / the SelfPost address / a typed
address, no user default); the account and out-domain-settings mockups
are redrawn for it in the change that follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner's instruction (2026-10-09): in the kit Sign out had no hover tint and
stood out among Account and Help. The cause was in the design, not in the
implementation: the mockups drew Sign out as a link, the panel signs out
with a POST, and Bulma tints only a.navbar-item.
- build.py: Sign out is a form with a submit button carrying navbar-item,
as the real shell has to write it; all pages rebuilt.
- theme.css: the form stretches the button across the menu and the button
is handed Bulma's own hover / focus / active variables, so it answers
like the entries above it. No width, no colour of its own.
check.py: 32 screens, 48 sp- classes, 0 violation(s); outlines.json unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review pass over `1.4.0`…`1.9.1`: port 25 no longer shares the submission milter chain, DMARC reports are validated before they are stored, and the auto rate limit is correct for a non-default level-1 window. Schema migration `0010` (index only). Upgrading is a tag bump.
Added:
- SQLite migration `0010_send_log_app_login_index.sql` —
- DMARC aggregate reports are accepted as **zip** as well as gzip or plain XML
- e2e: `checkPort25ConfigCombinations` regenerates the Postfix configuration
Changed:
- README hero line and a **Positioning** block in `docs/product.md` record the
Fixed:
- The setup account's password is hashed at bcrypt cost `12` like every other
- Login/setup rate limiter: protecting blocked buckets from eviction is now
- `postqueue -p` output is bounded to 4 MiB (the timeout alone left the size of
- [schema-migrations.md](docs/schema-migrations.md): the schema section was
- postqueue reader is now time-bounded: `postqueue -p` runs under a 5-second
… (11 more in CHANGELOG)
Changed:
- [roadmap.md](docs/roadmap.md) now carries everything the 2026-08-19 code
- Extracted `internal/configsafe` — the empty-value and forbidden-character
- Build, deploy and migration comments no longer cite the archived
- Dropped the `parseDomainRateLimitForm` / `parseAppRateLimitForm` pass-through
- Extracted shared helpers: `internal/atomicfile` (atomic temp-write+rename) and
… (7 more in CHANGELOG)
Removed:
- Unused legacy asset `docs/assets/icon.png` (superseded by the four brand SVGs
- Plan documents for already-shipped features: `domain-stats-auto-ratelimit`,
Full detail: CHANGELOG.md (1.9.2).