panel: match monitoring URLs to their nav labels

/sendlog -> /deliveries, /queue -> /mail-queue, /logtail -> /system-log,
along with the HTMX polling fragments under each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 21:41:52 +03:00
parent 89447074c5
commit a06faac213
12 changed files with 31 additions and 23 deletions
+7
View File
@@ -5,6 +5,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
## [Unreleased]
### Changed
- panel: the three monitoring pages now live at URLs that match their nav
labels — Deliveries at `/deliveries` (was `/sendlog`), Mail queue at
`/mail-queue` (was `/queue`), System log at `/system-log` (was `/logtail`).
Bookmarks to the old paths stop working.
## [0.2.0] - 2026-08-03
- panel: every authenticated page now ends with the running version
+2 -2
View File
@@ -77,8 +77,8 @@
1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7
([specification.md:427](specification.md)). В README нет ни слова про
`/status` (проверки PTR/hostname), `/sendlog` (журнал отправки, фильтры),
`/queue` (очередь Postfix), `/logtail` (хвост `mail.log`), `/reload`,
`/status` (проверки PTR/hostname), `/deliveries` (журнал отправки, фильтры),
`/mail-queue` (очередь Postfix), `/system-log` (хвост `mail.log`), `/reload`,
`/account`, `/backup` — при том что всё это реализовано
([internal/web/web.go:152](../internal/web/web.go:152)–188). Не описана и
процедура апгрейда (бамп тега → `docker compose up -d`), хотя раздел
+2 -1
View File
@@ -15,7 +15,8 @@ const usernameKey ctxKey = 0
//
// It also extends the sliding session (plan B.1) on activity, defined as
// everything except a GET request carrying HX-Request: the four monitoring
// fragments (/status/fragment, /queue/body, /logtail/body, /sendlog/rows)
// fragments (/status/fragment, /mail-queue/body, /system-log/body,
// /deliveries/rows)
// poll every 5s regardless of whether anyone is looking at the tab, so
// counting those as activity would make "N days idle" mean "N days since a
// browser tab was last open" instead.
+3 -3
View File
@@ -38,9 +38,9 @@
<div class="links">
{{if eq .Active "status"}}<span aria-current="page">Status</span>{{else}}<a href="/status">Status</a>{{end}}
{{if eq .Active "domains"}}<span aria-current="page">Domains</span>{{else}}<a href="/domains">Domains</a>{{end}}
{{if eq .Active "sendlog"}}<span aria-current="page">Deliveries</span>{{else}}<a href="/sendlog">Deliveries</a>{{end}}
{{if eq .Active "queue"}}<span aria-current="page">Mail queue</span>{{else}}<a href="/queue">Mail queue</a>{{end}}
{{if eq .Active "logtail"}}<span aria-current="page">System log</span>{{else}}<a href="/logtail">System log</a>{{end}}
{{if eq .Active "sendlog"}}<span aria-current="page">Deliveries</span>{{else}}<a href="/deliveries">Deliveries</a>{{end}}
{{if eq .Active "queue"}}<span aria-current="page">Mail queue</span>{{else}}<a href="/mail-queue">Mail queue</a>{{end}}
{{if eq .Active "logtail"}}<span aria-current="page">System log</span>{{else}}<a href="/system-log">System log</a>{{end}}
{{if eq .Active "backup"}}<span aria-current="page">Backup</span>{{else}}<a href="/backup">Backup</a>{{end}}
</div>
<div class="session">
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "logtail_body"}}
<div id="logtail-body" hx-get="/logtail/body" hx-trigger="every 5s" hx-swap="outerHTML">
<div id="logtail-body" hx-get="/system-log/body" hx-trigger="every 5s" hx-swap="outerHTML">
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
{{end}}{{else}}No log lines yet.{{end}}</span>
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "queue_body"}}
<div id="queue-body" hx-get="/queue/body" hx-trigger="every 5s" hx-swap="outerHTML">
<div id="queue-body" hx-get="/mail-queue/body" hx-trigger="every 5s" hx-swap="outerHTML">
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="card">
<h2>Filter</h2>
<form method="get" action="/sendlog">
<form method="get" action="/deliveries">
<label for="domain">Domain</label>
<select id="domain" name="domain">
<option value="">All domains</option>
+3 -3
View File
@@ -1,5 +1,5 @@
{{define "sendlog_rows"}}
<div id="sendlog-rows" hx-get="/sendlog/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
<div id="sendlog-rows" hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
hx-trigger="every 5s" hx-swap="outerHTML">
{{if .Rows}}
<table>
@@ -22,8 +22,8 @@
</table>
<p class="muted">
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
{{if .HasPrev}} &middot; <a href="/sendlog?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .HasNext}} &middot; <a href="/sendlog?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.NextPage}}">Older &rarr;</a>{{end}}
{{if .HasPrev}} &middot; <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .HasNext}} &middot; <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.NextPage}}">Older &rarr;</a>{{end}}
</p>
{{else}}
<p class="muted">No messages logged yet.</p>
+1 -1
View File
@@ -31,7 +31,7 @@
<p class="error">{{.QueueError}}</p>
{{else}}
<p>{{if .QueueSummary}}{{.QueueSummary}}{{else}}Mail queue is empty.{{end}}</p>
<a class="btn" href="/queue">Full queue</a>
<a class="btn" href="/mail-queue">Full queue</a>
{{end}}
</div>
+2 -2
View File
@@ -108,10 +108,10 @@ func TestNavMarksActivePage(t *testing.T) {
if !strings.Contains(out, `<span aria-current="page">Mail queue</span>`) {
t.Errorf("active page is not marked:\n%s", out)
}
if strings.Contains(out, `href="/queue"`) {
if strings.Contains(out, `href="/mail-queue"`) {
t.Errorf("active page still links to itself:\n%s", out)
}
if !strings.Contains(out, `href="/sendlog"`) {
if !strings.Contains(out, `href="/deliveries"`) {
t.Errorf("inactive pages are not linked:\n%s", out)
}
}
+6 -6
View File
@@ -180,12 +180,12 @@ func (s *Server) Handler() http.Handler {
// Monitoring screens (spec 7.2.11-13): each page and its HTMX polling
// fragment (spec 7.1 — the /rows and /body endpoints return HTML, not JSON).
authed.HandleFunc("GET /sendlog", s.handleSendLog)
authed.HandleFunc("GET /sendlog/rows", s.handleSendLogRows)
authed.HandleFunc("GET /queue", s.handleQueue)
authed.HandleFunc("GET /queue/body", s.handleQueueBody)
authed.HandleFunc("GET /logtail", s.handleLogTail)
authed.HandleFunc("GET /logtail/body", s.handleLogTailBody)
authed.HandleFunc("GET /deliveries", s.handleSendLog)
authed.HandleFunc("GET /deliveries/rows", s.handleSendLogRows)
authed.HandleFunc("GET /mail-queue", s.handleQueue)
authed.HandleFunc("GET /mail-queue/body", s.handleQueueBody)
authed.HandleFunc("GET /system-log", s.handleLogTail)
authed.HandleFunc("GET /system-log/body", s.handleLogTailBody)
mux.Handle("/", s.requireAuth(authed))
+2 -2
View File
@@ -187,10 +187,10 @@ func (c *panelClient) setRateLimit(path, allowedIP string, maxMessages, windowSe
return nil
}
// sendLogRows returns the raw /sendlog/rows HTML fragment, filtered to one
// 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("/sendlog/rows?domain=" + url.QueryEscape(domain))
_, body, err := c.get("/deliveries/rows?domain=" + url.QueryEscape(domain))
return body, err
}