Compare commits

..

3 Commits

Author SHA1 Message Date
mix 7cc499d57c panel: show the send log's status as a badge
test / test (push) Has been cancelled
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>
2026-08-11 01:23:52 +03:00
mix aeba3f8db1 panel: put the narrow pages' column back, and centre the badge text
Two fixes in one file, both from looking at the result.

Settings and the user form were narrowed to their card's width so their heading
would stop floating beside it. The shell centres the navigation column and the
page as a pair, so narrowing the page moved the navigation with it — 296px
sideways between Settings and Domains, which is the jump the width rules at the
top of this file exist to prevent, arrived at from the other direction. Every
way of aligning the heading with the card moves something else, so the geometry
goes back to what it was before the restyle, rendered against the previous
stylesheet page by page to confirm it. Those two headings are still not aligned
with their cards; that is worth fixing on its own terms rather than by making
the chrome move on every page.

And the badge text sat above the middle of its box: the badge inherited the
body's 1.5 line-height, and a lowercase word with no descender — ok, warn,
unknown — sits high in a line box that tall, which inside a border reads as
text stuck to the top. The box now hugs its line and the padding does the
centring, uneven on purpose to pay for the descender space the words rarely
use. The one that does, queued, keeps the same box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 01:23:37 +03:00
mix d6e67ceb61 panel: set Sign out like the entries it stands among
The compact button rule is written for controls that cluster — several beside a
value or inside a table cell — where 0.8rem/600 is what stops four of them
shouting at once. Sign out borrows that rule for its colours, and was taking
the type with it: in the navigation column it has no cluster to belong to, so
it stood under Settings among entries set 0.95rem/400 as the one smaller,
heavier word on the column, reading as a different kind of object rather than
as the last item of the list.

Type and padding go back to the entries'. The red, the border and the tinted
background stay — those are what say it acts, and the restyle only made the
mismatch easier to see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 00:59:08 +03:00
4 changed files with 63 additions and 20 deletions
+7 -1
View File
@@ -28,7 +28,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
whose only card is the narrow one now takes its heading and footer down to the whose only card is the narrow one now takes its heading and footer down to the
card's width instead of splitting them across the wider measure, and the send card's width instead of splitting them across the wider measure, and the send
log stops breaking `Details` and `deferred` across two lines when a row is log stops breaking `Details` and `deferred` across two lines when a row is
tight. The three tight.
- panel: the send log's **status is a badge**, in the same ok/warn/error/unknown
colours the status page and the DNS checks use, instead of the one place in
the panel where a status was bare text. The mapping is the one the delivery
page already applied — `sent` is ok, `deferred` a warning, `bounced` and
`rejected` errors, `queued` unknown because nothing has gone wrong yet.
The three
WOFF2 files add ~76 KB to the image and are served from the panel's own origin, WOFF2 files add ~76 KB to the image and are served from the panel's own origin,
so the Content-Security-Policy is unchanged (`default-src 'self'`). so the Content-Security-Policy is unchanged (`default-src 'self'`).
+14 -1
View File
@@ -145,6 +145,17 @@ func deliveryLevel(status string) string {
} }
} }
// sendLogRow is a row of the send log as the table draws it: the stored row
// plus the badge level its status maps onto. The level is carried rather than
// derived in the template because deliveryLevel is the one place that decides
// what a status means — the delivery page already reads it, and a second
// mapping written in the template or the stylesheet would be free to drift
// from it.
type sendLogRow struct {
store.SendLogRow
Level string // ok / warn / error / unknown, as deliveryLevel returns
}
// deliveryEvent is one step of a message's history, as the timeline on the // deliveryEvent is one step of a message's history, as the timeline on the
// delivery page draws it. At is zero for the step that has not happened yet — // delivery page draws it. At is zero for the step that has not happened yet —
// the delivery report a queued message is still waiting for. // the delivery report a queued message is still waiting for.
@@ -324,8 +335,10 @@ func (h *Handlers) sendLogData(r *http.Request) (map[string]any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
view := make([]sendLogRow, len(rows))
for i := range rows { for i := range rows {
rows[i].Subject = mailhdr.DecodeSubject(rows[i].Subject) rows[i].Subject = mailhdr.DecodeSubject(rows[i].Subject)
view[i] = sendLogRow{SendLogRow: rows[i], Level: deliveryLevel(rows[i].Status)}
} }
domainNames := make([]string, 0, len(assigned)) domainNames := make([]string, 0, len(assigned))
@@ -358,7 +371,7 @@ func (h *Handlers) sendLogData(r *http.Request) (map[string]any, error) {
lastPage = int((total + sendLogPageSize - 1) / sendLogPageSize) lastPage = int((total + sendLogPageSize - 1) / sendLogPageSize)
} }
return map[string]any{ return map[string]any{
"Rows": rows, "Rows": view,
"FilterDomains": domainNames, "FilterDomains": domainNames,
"FilterApps": logins, "FilterApps": logins,
"FilterDomain": filter.Domain, "FilterDomain": filter.Domain,
+41 -17
View File
@@ -194,17 +194,19 @@ a { color: var(--accent-text); }
/* form.inline only exists so a POST can sit next to other content without a /* form.inline only exists so a POST can sit next to other content without a
form's block layout; its button is styled like any other. */ form's block layout; its button is styled like any other. */
form.inline { display: inline; margin: 0; } form.inline { display: inline; margin: 0; }
/* A page whose card is the narrow one takes the column down with it. .card.narrow /* The signed-out pages are a single card and nothing else, and .card.narrow
centres itself inside whatever holds it, so at the panel's usual width the centres itself inside whatever holds it so at the panel's usual width the
card floated in the middle while the mark and the heading stayed at the far card floated in the middle while the mark and the heading stayed at the far
left three alignments on a page with four elements. Narrowing the column to left, three alignments on a page with four elements. Narrowing the column to
the card's own width lines the three up and puts the block as a whole in the the card's own width makes the three line up and puts the block as a whole in
middle of the page. the middle of the page.
Named by what the page holds rather than by which page it is: it was written By page name and not by "the page holds a narrow card", which is what this
for login and setup, and Settings and the user form have the same single briefly was. Settings and the user form hold one too, and narrowing their
narrow card and had the same split heading a list of page names would have column moves the navigation sideways with it the shell centres the column
had to be remembered and extended by every page added since. */ and the page as a pair, so 296px of it, measured. Their heading and card are
main:has(> .card.narrow) { max-width: 24rem; } still not aligned with each other; that is worth fixing on its own terms and
not by making every page's chrome move. */
main.page-login, main.page-setup { max-width: 24rem; }
.card + .card { margin-top: 1.2rem; } .card + .card { margin-top: 1.2rem; }
.flash { background: var(--flash-bg); border: 1px solid var(--flash-border); color: var(--flash-fg); padding: 0.7rem 1rem; border-radius: 6px; margin-bottom: 1.2rem; } .flash { background: var(--flash-bg); border: 1px solid var(--flash-border); color: var(--flash-fg); padding: 0.7rem 1rem; border-radius: 6px; margin-bottom: 1.2rem; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
@@ -261,9 +263,9 @@ h2 { font-size: 1.05rem; font-weight: 600; margin: 0 0 0.4rem; }
.version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: var(--muted); line-height: 1.45; } .version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: var(--muted); line-height: 1.45; }
.version a { color: inherit; text-decoration: underline; text-underline-offset: 2px; } .version a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.version a:hover { color: var(--fg); } .version a:hover { color: var(--fg); }
/* The narrow column is 24rem; a right-aligned multi-link notice wraps into a /* The signed-out column is 24rem; a right-aligned multi-link notice wraps
ragged edge, so centre it there. */ into a ragged edge, so centre it there. */
main:has(> .card.narrow) .version { text-align: center; } main.page-login .version, main.page-setup .version { text-align: center; }
select, textarea { select, textarea {
width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem; width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem;
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit; border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
@@ -391,8 +393,9 @@ button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg); font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg);
box-shadow: inset 2px 0 0 var(--accent-fill); box-shadow: inset 2px 0 0 var(--accent-fill);
} }
/* Sign out carries an icon too, so it needs the same row layout; its padding /* Sign out carries an icon too, so it needs the same row layout; its colours
and colours come from the compact button rule further down. */ come from the compact button rule further down, and its type is put back to
the column's beside it (see the rule after that one). */
.nav button { display: flex; align-items: center; gap: 0.5rem; } .nav button { display: flex; align-items: center; gap: 0.5rem; }
/* The icons draw in the entry's own colour, so the active entry's darker text, /* The icons draw in the entry's own colour, so the active entry's darker text,
a link's blue and Sign out's red all carry through without a rule apiece. */ a link's blue and Sign out's red all carry through without a rule apiece. */
@@ -460,9 +463,19 @@ button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
is where the panel puts machine output everywhere else. Uppercasing it was is where the panel puts machine output everywhere else. Uppercasing it was
the pill's own emphasis dropped with the pill, since the colour already the pill's own emphasis dropped with the pill, since the colour already
says how loud the badge is. */ says how loud the badge is. */
/* line-height 1 and the padding doing the centring, rather than the 1.5 the
badge would inherit from the body. In a tall line box a word of lowercase
with no descender ok, warn, unknown sits above the optical centre of the
space around it, and inside a bordered box that reads as text stuck to the
top. With the box hugging the line, the padding is what centres the word, and
it is uneven on purpose: the extra tenth at the top pays for the descender
space every line box reserves below the baseline and these words rarely use.
The one that does use it, queued, keeps the same box the tail hangs into
the bottom padding rather than growing the badge. */
.st { .st {
display: inline-block; padding: 0.1rem 0.45rem; border-radius: 4px; display: inline-block; padding: 0.28rem 0.45rem 0.2rem; border-radius: 4px;
font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; letter-spacing: 0.02em; font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1;
letter-spacing: 0.02em;
vertical-align: middle; border: 1px solid transparent; vertical-align: middle; border: 1px solid transparent;
} }
.st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); } .st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); }
@@ -600,6 +613,17 @@ button.copy { flex: none; margin-top: 0.3rem; }
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
} }
.actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); } .actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); }
/* Sign out takes the compact button's colours but not its type. The rule above
is written for controls that cluster a row of them beside a value or in a
table cell where 0.8rem/600 is what keeps four of them from shouting. In
the navigation column it has no cluster to belong to: it stands under
Settings, among entries set 0.95rem/400, and being smaller and heavier than
every word around it made it read as a different kind of object rather than
as the last item of the list. Type and padding go back to the entries';
the red, the border and the background stay, and are what say it acts. */
.nav button {
padding: 0.35rem 0.6rem; font-size: 0.95rem; font-weight: 400;
}
/* The optional "encrypt this download" block on the backup and export forms. /* The optional "encrypt this download" block on the backup and export forms.
Its label is the one checkbox in the panel, so it opts out of the Its label is the one checkbox in the panel, so it opts out of the
@@ -13,7 +13,7 @@
<td>{{.From}}</td> <td>{{.From}}</td>
<td>{{.To}}</td> <td>{{.To}}</td>
<td class="subject"><span title="{{.Subject}}">{{.Subject}}</span></td> <td class="subject"><span title="{{.Subject}}">{{.Subject}}</span></td>
<td class="status">{{.Status}}</td> <td class="status"><span class="st st-{{.Level}}">{{.Status}}</span></td>
<td class="actions"><a href="/deliveries/{{.ID}}?domain={{$.FilterDomain}}&app={{$.FilterApp}}&p={{$.Page}}">Details</a></td> <td class="actions"><a href="/deliveries/{{.ID}}?domain={{$.FilterDomain}}&app={{$.FilterApp}}&p={{$.Page}}">Details</a></td>
</tr> </tr>
{{end}} {{end}}