panel-notifications: Account, Server > Settings, Server > Health, help; e2e
test / test (push) Canceled after 0s

The pages of the mockups accepted in 87fa0a4, built from the kit's partials
(written by a Sonnet agent into the working tree, reviewed here):

- Account: "Send me notifications" in the Profile form, saved with the
  profile; an unticked box is off.
- Server > Settings: the instance switch (box 03), and box 04 Notification
  mail with the DKIM and SPF records of the server's own name, drawn with the
  record partial of a domain's DNS page and checked in DNS. Sending rate
  limits are box 02 in the left column. A record that is not in place is
  tagged as a warning, not a failure: notifications are sent without it.
- Server > Health: card 06 Notification mail with the state of each record
  and a link to Settings, beside Configuration (07); part of the polled
  fragment; Re-check DNS forces it too. Overview does not run this check.
- help.go: the Notifications topic and the clauses in Settings and Your
  account, word for word with help.html.

If the server's key cannot be read the pages still render: the DKIM record
is left out and SPF is shown unchecked.

e2e: the administrator gets a mailbox behind the sink and the server's DKIM
record from Settings is published; the level-2 refusal of the next step must
then arrive as a notification from postmaster@mail.e2e.test, signed as that
name with the key Settings shows, and must not be a row of the Outbound log.
The rotation step now waits for Postfix to be running: it follows a step that
restarts the container, and logrotate's `postfix reload` failed in between.

On the test server, for this tree: go vet clean, go test ./... 23 packages
ok, make e2e ok (147 s); the notification reached the sink 28 s after the
refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-12 16:21:48 +03:00
co-authored by Claude Opus 5.5
parent c9027381e5
commit b905f3e560
22 changed files with 1003 additions and 59 deletions
+34
View File
@@ -19,6 +19,9 @@ type Account struct {
Profile Box
Username string
Email string
// Notify is whether the panel writes to this user: the checkbox under the
// e-mail.
Notify bool
Password Box
}
@@ -63,6 +66,18 @@ type Settings struct {
RateLimits Box
RateFacts []Fact
// Notifications is the instance switch for notification e-mails; Notify is
// its state and NotifyNote the line under it, which says from where and to
// whom.
Notifications Box
Notify bool
NotifyNote Text
// Mail holds the DNS records of the server's own name, which notifications
// are signed and authorised with, drawn like a domain's.
Mail Box
MailRecords []Record
}
// NewSettings builds the page. retention is what the field shows (the stored
@@ -87,9 +102,28 @@ func NewSettings(m Meta, retention, level1 string) *Settings {
{Label: "Level 2 · domain, application",
Value: Rich("Set on each ", Link("/outbound/domains", "domain"), " and on each application of a domain.")},
},
Notifications: Box{No: "03", Title: "Notifications", ID: "notifications", Help: topicLink(HelpNotifications, "What is sent and when")},
Mail: Box{No: "04", Title: "Notification mail", ID: "notification-mail", End: Plain("DNS records of the server's own name"),
Help: topicLink(HelpNotifications, "What the panel sends and what makes it arrive")},
}
}
// WithNotifications sets the instance switch: on is whether notification
// e-mails are sent at all, from is the address they come from.
func (s *Settings) WithNotifications(on bool, from string) *Settings {
s.Notify = on
s.NotifyNote = Rich("From ", Mono(from), ", to every user who has an e-mail and has not turned them off under ",
Link("/account", "Account"), ".")
return s
}
// WithMail sets the records of the server's own name, in the order they are
// shown (ServerMailDKIM, ServerMailSPF).
func (s *Settings) WithMail(records []Record) *Settings {
s.MailRecords = records
return s
}
// WithInbound adds the retention of the Inbound log to the Log retention box.
// days is what the field shows, like retention in NewSettings. The field exists
// only while the inbound relay is on: with it off there is no journal to keep.