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:
@@ -5,6 +5,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.2.0] - 2026-08-03
|
||||||
|
|
||||||
- panel: every authenticated page now ends with the running version
|
- panel: every authenticated page now ends with the running version
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
|
|
||||||
1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7
|
1. **Нет раздела «эксплуатация»** — прямое требование ТЗ 11 п. 7
|
||||||
([specification.md:427](specification.md)). В README нет ни слова про
|
([specification.md:427](specification.md)). В README нет ни слова про
|
||||||
`/status` (проверки PTR/hostname), `/sendlog` (журнал отправки, фильтры),
|
`/status` (проверки PTR/hostname), `/deliveries` (журнал отправки, фильтры),
|
||||||
`/queue` (очередь Postfix), `/logtail` (хвост `mail.log`), `/reload`,
|
`/mail-queue` (очередь Postfix), `/system-log` (хвост `mail.log`), `/reload`,
|
||||||
`/account`, `/backup` — при том что всё это реализовано
|
`/account`, `/backup` — при том что всё это реализовано
|
||||||
([internal/web/web.go:152](../internal/web/web.go:152)–188). Не описана и
|
([internal/web/web.go:152](../internal/web/web.go:152)–188). Не описана и
|
||||||
процедура апгрейда (бамп тега → `docker compose up -d`), хотя раздел
|
процедура апгрейда (бамп тега → `docker compose up -d`), хотя раздел
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const usernameKey ctxKey = 0
|
|||||||
//
|
//
|
||||||
// It also extends the sliding session (plan B.1) on activity, defined as
|
// It also extends the sliding session (plan B.1) on activity, defined as
|
||||||
// everything except a GET request carrying HX-Request: the four monitoring
|
// 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
|
// 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
|
// counting those as activity would make "N days idle" mean "N days since a
|
||||||
// browser tab was last open" instead.
|
// browser tab was last open" instead.
|
||||||
|
|||||||
@@ -38,9 +38,9 @@
|
|||||||
<div class="links">
|
<div class="links">
|
||||||
{{if eq .Active "status"}}<span aria-current="page">Status</span>{{else}}<a href="/status">Status</a>{{end}}
|
{{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 "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 "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="/queue">Mail queue</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="/logtail">System log</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}}
|
{{if eq .Active "backup"}}<span aria-current="page">Backup</span>{{else}}<a href="/backup">Backup</a>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="session">
|
<div class="session">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "logtail_body"}}
|
{{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}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
||||||
{{end}}{{else}}No log lines yet.{{end}}</span>
|
{{end}}{{else}}No log lines yet.{{end}}</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "queue_body"}}
|
{{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}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Filter</h2>
|
<h2>Filter</h2>
|
||||||
<form method="get" action="/sendlog">
|
<form method="get" action="/deliveries">
|
||||||
<label for="domain">Domain</label>
|
<label for="domain">Domain</label>
|
||||||
<select id="domain" name="domain">
|
<select id="domain" name="domain">
|
||||||
<option value="">All domains</option>
|
<option value="">All domains</option>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "sendlog_rows"}}
|
{{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">
|
hx-trigger="every 5s" hx-swap="outerHTML">
|
||||||
{{if .Rows}}
|
{{if .Rows}}
|
||||||
<table>
|
<table>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
</table>
|
</table>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
|
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
|
||||||
{{if .HasPrev}} · <a href="/sendlog?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
{{if .HasPrev}} · <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
||||||
{{if .HasNext}} · <a href="/sendlog?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.NextPage}}">Older →</a>{{end}}
|
{{if .HasNext}} · <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.NextPage}}">Older →</a>{{end}}
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="muted">No messages logged yet.</p>
|
<p class="muted">No messages logged yet.</p>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<p class="error">{{.QueueError}}</p>
|
<p class="error">{{.QueueError}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>{{if .QueueSummary}}{{.QueueSummary}}{{else}}Mail queue is empty.{{end}}</p>
|
<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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ func TestNavMarksActivePage(t *testing.T) {
|
|||||||
if !strings.Contains(out, `<span aria-current="page">Mail queue</span>`) {
|
if !strings.Contains(out, `<span aria-current="page">Mail queue</span>`) {
|
||||||
t.Errorf("active page is not marked:\n%s", out)
|
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)
|
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)
|
t.Errorf("inactive pages are not linked:\n%s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -180,12 +180,12 @@ func (s *Server) Handler() http.Handler {
|
|||||||
|
|
||||||
// Monitoring screens (spec 7.2.11-13): each page and its HTMX polling
|
// 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).
|
// fragment (spec 7.1 — the /rows and /body endpoints return HTML, not JSON).
|
||||||
authed.HandleFunc("GET /sendlog", s.handleSendLog)
|
authed.HandleFunc("GET /deliveries", s.handleSendLog)
|
||||||
authed.HandleFunc("GET /sendlog/rows", s.handleSendLogRows)
|
authed.HandleFunc("GET /deliveries/rows", s.handleSendLogRows)
|
||||||
authed.HandleFunc("GET /queue", s.handleQueue)
|
authed.HandleFunc("GET /mail-queue", s.handleQueue)
|
||||||
authed.HandleFunc("GET /queue/body", s.handleQueueBody)
|
authed.HandleFunc("GET /mail-queue/body", s.handleQueueBody)
|
||||||
authed.HandleFunc("GET /logtail", s.handleLogTail)
|
authed.HandleFunc("GET /system-log", s.handleLogTail)
|
||||||
authed.HandleFunc("GET /logtail/body", s.handleLogTailBody)
|
authed.HandleFunc("GET /system-log/body", s.handleLogTailBody)
|
||||||
|
|
||||||
mux.Handle("/", s.requireAuth(authed))
|
mux.Handle("/", s.requireAuth(authed))
|
||||||
|
|
||||||
|
|||||||
@@ -187,10 +187,10 @@ func (c *panelClient) setRateLimit(path, allowedIP string, maxMessages, windowSe
|
|||||||
return nil
|
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.
|
// domain, for polling a row's status without parsing full HTML into structs.
|
||||||
func (c *panelClient) sendLogRows(domain string) (string, error) {
|
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
|
return body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user