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>
This commit is contained in:
2026-08-11 01:23:52 +03:00
parent aeba3f8db1
commit 7cc499d57c
3 changed files with 22 additions and 3 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,
@@ -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}}