panel-redesign: Overview, Health, Account and Server > Settings on the kit
Stage 2, step 2. Written by Sonnet, reviewed by Opus.
- Four pages named after their mockups: overview, health, account,
settings, each with a typed page struct (view/overview.go, health.go,
account.go), a fixture and tests. The engine page "status" is gone
(status.html, status_body.html deleted); "settings" is Server >
Settings only. status and settings are off legacy_pages.txt.
- Overview: the stamp and its sentence are derived from the six cards
(the worst card sets the level and the lead names the cards that are
not fine); the outbound and inbound domain tables are built from the
existing service calls. Without the inbound relay the outbound box
takes the full width.
- Both pages refresh themselves as the old Status page did. Health
polls its first row of boxes and swaps the second out of band.
Overview polls its Server health box and the page head follows out of
band (GET /overview/fragment), so stamp, sentence and time move with
the cards.
- Kit, attributes only (no class, no CSS, markup unchanged when empty):
Box.Poll and Head.ID / Head.OOB.
- healthCards: the Queue card says "Empty" / "N queued" instead of
postqueue's line, which was clipped in a card; Processes and Milter
sockets carry a sub line. OverallStatus / OverallHeading are gone.
- Handlers pass typed data; routes, POST targets, field names,
validation and authorization are unchanged.
States the mockups do not draw, all from existing partials: errors and
successes as the kit flash above the boxes; Health with an unreadable
process list, a certificate without facts, a missing host name; Account
with the report authorization not published / not found / not checked
and with hosted reports off; empty domain tables as empty_state.
Differences from the mockups, for the owner:
- Overview title is one of "Everything is running normally." /
"Running, with warnings." / "A component needs attention.", not
"Mail is flowing..." (the checks do not prove that); the note says
"Updated HH:MM:SS UTC" without an interval, Health's lead says
"Refreshes automatically" (panel.js polls at 5 s active, 30 s idle);
- one Queue card ("outbound"), two milter sockets: what this build has;
- the days column is the stats window, min(30, retention);
- Health: no "check port 80" paragraph (the certificate's own detail
says what to check); a failing socket shows its detail under the path;
figures come from the sampler ("12%", "MiB");
- Account: the DMARC form keeps the address field for "Another
address" (hidden unless chosen); the count "N of M now" is the
domains following this user's default;
- Settings: Level 2 links the domain list only;
- /help#checks and /help#dmarc open Help at the top until the Help step.
Evidence
- docs/assets/panel-redesign/evidence/overview.png, health.png,
account.png, settings.png: 1440 px wide, full height, headless Edge,
real templates served locally with their fixtures - NOT from the
container (no Docker on this machine).
- panel/audit.js on the four pages at 1440 and 1280: 612 texts
checked, problems: [].
- Polling was exercised in a browser against a local fixture server
with document.hidden overridden (the pane was hidden): one element
per id after a swap, no leftover hx-swap-oob, about one request per
5 s. The first request of a visible tab was not observed.
- go test -count=1 ./internal/web/...
ok internal/web
ok internal/web/auth
ok internal/web/handlers
ok internal/web/validate
ok internal/web/view
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,152 +1,30 @@
|
||||
{{/* One legacy template for two pages until stage 2 gives each its own:
|
||||
Account (GET /account, every role) — the user's name and e-mail, their
|
||||
password and their default DMARC report address, each its own form — and
|
||||
Server › Settings (GET /server/settings, global role) — what holds for the
|
||||
whole instance. .AccountPage says which one this is. */}}
|
||||
{{define "wide"}}wide{{end}}
|
||||
|
||||
{{/* Server > Settings (mockup settings.html): what holds for the whole instance.
|
||||
One form, one Save button; the rate limits are read-only facts. A user's own
|
||||
name, e-mail, password and DMARC address are on Account (account.html). */}}
|
||||
{{define "content"}}
|
||||
{{if .AccountPage}}
|
||||
<h1>Account</h1>
|
||||
{{template "page_head" .Head}}
|
||||
{{if .Flash}}{{template "flash" .Flash}}{{end}}
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
|
||||
<div class="split">
|
||||
<div class="card" id="profile">
|
||||
<h2>Profile</h2>
|
||||
<p class="muted">Signed in as {{.User}} · {{.Role}}. These are the
|
||||
credentials for this control panel only. Applications keep their own logins
|
||||
and passwords, which are not affected.</p>
|
||||
<form method="post" action="/account/profile">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required>
|
||||
|
||||
<label for="email">E-mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormEmail}}">
|
||||
<p class="muted">Where the panel writes to you. It can also receive DMARC
|
||||
reports — your choice, under DMARC reports.</p>
|
||||
|
||||
<button type="submit">Save profile</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" id="password">
|
||||
<h2>Password</h2>
|
||||
<form method="post" action="/account/password">
|
||||
<label for="current_password">Current password</label>
|
||||
<input id="current_password" name="current_password" type="password"
|
||||
autocomplete="current-password" required>
|
||||
|
||||
<label for="new_password">New password</label>
|
||||
<input id="new_password" name="new_password" type="password" autocomplete="new-password" required>
|
||||
|
||||
<label for="new_password_confirm">Repeat</label>
|
||||
<input id="new_password_confirm" name="new_password_confirm" type="password" autocomplete="new-password" required>
|
||||
|
||||
<button type="submit">Change password</button>
|
||||
</form>
|
||||
<p class="muted">Changing it signs out every other session of this account;
|
||||
this one stays signed in.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="dmarc">
|
||||
<h2>DMARC reports</h2>
|
||||
<p class="muted">Your default report address. It is used by every outbound
|
||||
domain whose report address is set to follow your default; changing it
|
||||
changes the DMARC record those domains must publish.</p>
|
||||
<form method="post" action="/account/dmarc">
|
||||
<label for="dmarc_default">My default report address</label>
|
||||
<select id="dmarc_default" name="dmarc_default" data-custom-mode="custom">
|
||||
{{if .DMARCIngestEnabled}}<option value="hosted"{{if eq .FormDMARCMode "hosted"}} selected{{end}}>SelfPost hosted — read them under DMARC</option>{{end}}
|
||||
<option value="account"{{if eq .FormDMARCMode "account"}} selected{{end}}>My account e-mail{{if .AccountEmail}} — {{.AccountEmail}}{{end}}</option>
|
||||
<option value="custom"{{if eq .FormDMARCMode "custom"}} selected{{end}}>Another address…</option>
|
||||
<option value="none"{{if eq .FormDMARCMode "none"}} selected{{end}}>No reports</option>
|
||||
</select>
|
||||
<div data-custom-address>
|
||||
<label for="dmarc_default_address">Report address</label>
|
||||
<input id="dmarc_default_address" name="dmarc_default_address" type="email"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
value="{{.FormDMARCAddress}}" placeholder="reports@your-mail-domain.com">
|
||||
<form class="sp-form" method="post" action="/server/settings">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
{{template "box_open" .Retention}}
|
||||
{{template "body_open"}}
|
||||
<div class="field"><label class="label" for="send_log_retention_days">Keep outbound log rows, days</label>
|
||||
<div class="control"><input class="input" id="send_log_retention_days" name="send_log_retention_days" type="number" min="7" max="365" step="1" value="{{.RetentionDays}}" required></div>
|
||||
<p class="help">The main driver of <code>/data</code> growth. Does not affect <code>mail.log</code> rotation (fourteen daily files).</p></div>
|
||||
{{template "body_close"}}
|
||||
{{template "box_close"}}
|
||||
</div>
|
||||
<button type="submit">Save report address</button>
|
||||
</form>
|
||||
|
||||
{{if .ReportAuthName}}
|
||||
<p class="muted">When <code>rua=</code> points at another domain, that hub
|
||||
domain must publish a report-authorisation record so receivers will deliver
|
||||
the XML aggregates.</p>
|
||||
|
||||
<label>Report authorization — host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
<div class="column is-6">
|
||||
{{template "box_open" .RateLimits}}
|
||||
{{template "body_open"}}
|
||||
{{template "facts" .RateFacts}}
|
||||
{{template "body_close"}}
|
||||
{{template "box_close"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Report authorization — value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthExample}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
|
||||
{{if .ReportAuthDNS.Status}}
|
||||
<label>Report authorization DNS <span class="st st-{{.ReportAuthDNS.Status}}">{{.ReportAuthDNS.Status}}</span></label>
|
||||
<p class="{{if eq .ReportAuthDNS.Status "ok"}}muted{{else}}error{{end}}">{{.ReportAuthDNS.Detail}}</p>
|
||||
{{if .ReportAuthDNS.Records}}<span class="code">{{range .ReportAuthDNS.Records}}{{.}}
|
||||
{{end}}</span>{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h1>Settings</h1>
|
||||
|
||||
<p class="muted">What holds for the whole instance, whoever is signed in. Your
|
||||
own name, e-mail, password and DMARC report address are under
|
||||
<a href="/account">Account</a>.</p>
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
|
||||
<form method="post" action="/server/settings">
|
||||
<div class="card" id="deliveries-retention">
|
||||
<h2>Deliveries</h2>
|
||||
<p class="muted">How long rows on <a href="/outbound/log">Deliveries</a> are
|
||||
kept before the background sweep deletes them — the main driver of
|
||||
<code>/data</code> growth over time. Does not affect <code>mail.log</code>
|
||||
rotation (fourteen daily files).</p>
|
||||
|
||||
<label for="send_log_retention_days">Send log retention (days)</label>
|
||||
<input id="send_log_retention_days" name="send_log_retention_days" type="number"
|
||||
min="7" max="365" step="1" required
|
||||
value="{{.FormSendLogRetentionDays}}">
|
||||
<button type="submit">Save settings</button>
|
||||
</div>
|
||||
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
||||
</form>
|
||||
|
||||
<div class="card" id="rate-limits">
|
||||
<h2>Sending rate limits</h2>
|
||||
<p class="muted">Configured in <code>.env</code> / Compose; restart the
|
||||
container to change level 1. Domain and application ceilings are set on
|
||||
each domain's page.</p>
|
||||
|
||||
<label>Level 1 — per client IP (Postfix)</label>
|
||||
<p class="code-row"><span class="code">{{.L1Messages}} messages / {{.L1Window}} seconds</span></p>
|
||||
<p class="muted"><code>RATE_LIMIT_MESSAGES_PER_IP</code> /
|
||||
<code>RATE_LIMIT_WINDOW_SECONDS</code>. Hard ceiling for every connecting IP;
|
||||
the panel cannot raise a domain or application limit above this.</p>
|
||||
|
||||
<label>Level 2 — domain</label>
|
||||
<p class="muted">Optional ceiling for <em>all</em> senders on a domain. When
|
||||
unset, only level 1 applies. Must be ≤ level 1.</p>
|
||||
|
||||
<label>Level 2 — application</label>
|
||||
<p class="muted">Overrides the domain limit for one application. The ceiling
|
||||
may be <em>above or below</em> the domain setting (≤ level 1). When unset,
|
||||
the domain limit or level 1 applies. Independent of the client IP
|
||||
allow-list.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user