release: 0.6.0: Delivery detail page, DNS badge, machine metrics, wide layout

A page per delivery (`/deliveries/{id}`), reached from the *Details* link on
Added:
  - A page per delivery (`/deliveries/{id}`), reached from the *Details* link on
  - A **DNS** badge in the domain list, one per row, carrying the same
  - Machine metrics on the status page: a **Machine** card reporting the
  - An index of the current page's own sections in the navigation column, for the
Changed:
  - Every panel page is laid out in one column of the same width, so moving
  - The mark's small-size variant — the tab icon and the `SP` initials it
  - The stamp's `SELF-HOSTED SMTP RELAY` line is set at 11.5/0.15 instead of
  - The delivery log lists what identifies a message and nothing else: time,
  - Subjects are decoded for display as well as on the way in, so the rows the
  … (4 more in CHANGELOG)

Full detail: CHANGELOG.md (0.6.0).
This commit is contained in:
mix
2026-09-22 15:24:51 +03:00
parent 5e5d157496
commit 83c488cc48
40 changed files with 2207 additions and 242 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ func TestE2E(t *testing.T) {
}
if err := waitFor("send-log row to reach status=sent", 30*time.Second, 500*time.Millisecond, func() (bool, error) {
rows, err := sc.panel.sendLogRows(senderDomain)
rows, err := sc.panel.sendLogRows(senderDomain, "")
if err != nil {
return false, err
}
+2 -3
View File
@@ -2,7 +2,6 @@ package e2e
import (
"fmt"
"strings"
"testing"
"time"
)
@@ -54,11 +53,11 @@ func testLevel2RateLimit(t *testing.T, sc *scenario) {
}
if err := waitFor("a rejected row for l2app in the send log", 15*time.Second, 500*time.Millisecond, func() (bool, error) {
rows, err := sc.panel.sendLogRows(senderDomain)
rows, err := sc.panel.sendLogRows(senderDomain, login)
if err != nil {
return false, err
}
if strings.Contains(rows, "l2app") && containsCell(rows, "rejected") {
if containsCell(rows, "rejected") {
return true, nil
}
return false, fmt.Errorf("no rejected row for l2app yet")
+18 -7
View File
@@ -189,9 +189,17 @@ func (c *panelClient) setRateLimit(path, allowedIP string, maxMessages, windowSe
}
// sendLogRows returns the raw /deliveries/rows HTML fragment, filtered to one
// domain, for polling a row's status without parsing full HTML into structs.
func (c *panelClient) sendLogRows(domain string) (string, error) {
_, body, err := c.get("/deliveries/rows?domain=" + url.QueryEscape(domain))
// domain and optionally to one application login, for polling a row's status
// without parsing full HTML into structs. The application is a filter rather
// than something to search the returned rows for: the log's columns identify a
// message (time, from, to, subject, status) and the sending application is only
// named on a row's own /deliveries/{id} page.
func (c *panelClient) sendLogRows(domain, app string) (string, error) {
q := url.Values{"domain": {domain}}
if app != "" {
q.Set("app", app)
}
_, body, err := c.get("/deliveries/rows?" + q.Encode())
return body, err
}
@@ -202,15 +210,18 @@ func (c *panelClient) status() (*http.Response, error) {
return resp, err
}
// applicationID scrapes an application's numeric id off its domain page row,
// keyed by login — needed to build /applications/{id}/ratelimit, which the
// add-application response (just the login/password) does not carry.
// applicationID scrapes an application's numeric id off its block on the domain
// page, keyed by login — needed to build /applications/{id}/ratelimit, which the
// add-application response (just the login/password) does not carry. The id is
// taken from the first action posted under that login, whichever it is, so
// reordering the block's controls does not break the scrape; only the login
// heading itself is anchored on.
func (c *panelClient) applicationID(domainID, login string) (string, error) {
_, body, err := c.get("/domains/" + domainID)
if err != nil {
return "", err
}
pattern := `(?s)<td class="code">` + regexp.QuoteMeta(login) + `</td>.*?/applications/(\d+)/mode`
pattern := `(?s)<p class="app-login">` + regexp.QuoteMeta(login) + `</p>.*?/applications/(\d+)/`
m := regexp.MustCompile(pattern).FindStringSubmatch(body)
if m == nil {
return "", fmt.Errorf("could not find application id for login %q", login)