Stage 1, step 3 (plan § Routes). BREAKING: every panel URL changes and the
old paths are removed without redirects.
Routes (web.go)
- /status -> /overview; /domains -> /outbound/domains with the domain's
settings actions under .../settings/ (reports, ratelimit, export);
/deliveries -> /outbound/log; /mail-queue -> /outbound/queue; /dmarc ->
/outbound/dmarc; /inbound -> /inbound/domains; /system-log ->
/server/log; /backup -> /server/backup (import under it); /users ->
/server/users; recheck and reload under /server/health/.
- One fragment name: <page>/fragment everywhere.
- Everything under /server/ is registered through server(), which wraps
the handler in globalOnly: the subtree is guarded once, where a new
route cannot forget it. Handlers keep their own requireGlobal.
- GET / leads to /overview, /outbound/domains or /inbound/domains.
- The /account -> /settings redirect is gone with /settings.
Account and Server > Settings
- handlers_account.go: GET /account and three POSTs - profile (username,
e-mail; no password asked, as designed), password (current password,
rate-limited like a sign-in, signs out other sessions), dmarc (the
user's default: hosted / account / custom / none, each validated).
This replaces step 1's bridge through the old Settings form.
- handlers_settings.go is the instance's page only: log retention and the
read-only rate limits, global role.
- One legacy template serves both pages by a flag until stage 2 gives each
its own: a new legacy page cannot be added to the ratchet.
Still to come inside stage 1, and why @routes stays on the ratchet:
- step 4: the application routes (one POST, under the domain);
- step 5: GET /server/health.
With @routes removed by hand TestRoutesFollowNavigation reports exactly
those and nothing else - no template links to a path the router lacks.
Old templates, tests and the e2e client are retargeted. The e2e module was
edited but not run here (no Docker, and its dependency could not be
downloaded on this machine).
Tests: Account (page, profile, password, DMARC default in every mode, a
domain administrator's own row only), Server settings (saved without a
password, range, global only), the /server/ subtree answered 404 by the
router for a domain administrator with All on both lists and for no
principal.
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail.
Roadmap 13/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
5.3 KiB
HTML
125 lines
5.3 KiB
HTML
{{/* One message's page. It takes the whole column rather than the reading
|
|
measure (see the "wide" block in layout.html): the two cards below stand
|
|
side by side, and under them are raw mail.log lines, which are long by
|
|
nature and were the reason the send log opted out too. */}}
|
|
{{define "wide"}}wide{{end}}
|
|
|
|
{{define "content"}}
|
|
{{/* The subject is the page's name — it is what the message was about, and
|
|
what an operator arriving from the log is holding in their head. The route
|
|
under it is the other half of the identity: who it was from and who it was
|
|
for, with the outcome badged on the end so the answer is on the first line
|
|
of the page rather than somewhere in the cards below. */}}
|
|
<h1 class="subject">{{if .Row.Subject}}{{.Row.Subject}}{{else}}(no subject){{end}}</h1>
|
|
<p class="route">
|
|
<span class="addr">{{.Row.From}}</span>
|
|
<span class="arrow" aria-hidden="true">→</span>
|
|
<span class="addr">{{if .Row.To}}{{.Row.To}}{{else}}—{{end}}</span>
|
|
<span class="st st-{{.Level}}">{{.Row.Status}}</span>
|
|
</p>
|
|
|
|
{{template "back_link" (back .BackURL "Back to deliveries")}}
|
|
|
|
{{/* The two columns: what was recorded on the left, in what order it happened
|
|
on the right. They are a pair — the facts are only worth reading against
|
|
the history and the history only means anything for a known message — so
|
|
they are read together rather than one after the other (.split in
|
|
panel.css lays them down into one column when there is no room). */}}
|
|
<div class="split">
|
|
<div class="card" id="message">
|
|
<h2>Message</h2>
|
|
<p class="muted">What the journal recorded as Postfix accepted this
|
|
message. The delivery log lists only what identifies a row at a glance;
|
|
everything the table has no column for is here.</p>
|
|
|
|
<div class="facts">
|
|
<div class="fact">
|
|
<span class="fact-label">Domain</span>
|
|
<span class="fact-value">{{if .Row.Domain}}{{.Row.Domain}}{{else}}—{{end}}</span>
|
|
</div>
|
|
<div class="fact">
|
|
<span class="fact-label">Application</span>
|
|
<span class="fact-value">{{if .Row.AppLogin}}{{.Row.AppLogin}}{{else}}—{{end}}</span>
|
|
</div>
|
|
<div class="fact">
|
|
<span class="fact-label">Accepted</span>
|
|
<span class="fact-value">{{.Row.CreatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
|
</div>
|
|
<div class="fact">
|
|
<span class="fact-label">Status reported</span>
|
|
<span class="fact-value">{{.Row.UpdatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
|
</div>
|
|
{{/* The queue id is monospace because it is an identifier to compare
|
|
character by character against a log line, not a word to read. */}}
|
|
<div class="fact">
|
|
<span class="fact-label">Queue id</span>
|
|
<span class="fact-value mono">{{if .Row.QueueID}}{{.Row.QueueID}}{{else}}—{{end}}</span>
|
|
</div>
|
|
<div class="fact">
|
|
<span class="fact-label">Journal id</span>
|
|
<span class="fact-value mono">{{.Row.ID}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{if not .Row.QueueID}}
|
|
<p class="muted">A message with no queue id was refused before Postfix
|
|
queued it — under a level-2 rate limit — so it has no delivery attempt to
|
|
show.</p>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="card" id="history">
|
|
<h2>History</h2>
|
|
<p class="muted">A row is written when the message is accepted and updated
|
|
once when Postfix reports the attempt for this recipient, so these are the
|
|
two moments the journal knows about — not a trace of the SMTP conversation,
|
|
which is in the delivery log below.</p>
|
|
|
|
<ol class="timeline">
|
|
{{range .Events}}
|
|
<li class="event lvl-{{.Level}}{{if .At.IsZero}} pending{{end}}">
|
|
<p class="event-time">{{if .At.IsZero}}not yet{{else}}{{.At.Format "2006-01-02 15:04:05"}} UTC{{end}}</p>
|
|
<p class="event-title"><span class="st st-{{.Level}}">{{.Status}}</span> {{.Title}}</p>
|
|
<p class="event-detail muted">{{.Detail}}</p>
|
|
</li>
|
|
{{end}}
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* Under both, at the column's full width: what Postfix itself wrote. The
|
|
queue id used to be printed on this page as something to go and search the
|
|
system log for by hand — this is that search, already done. */}}
|
|
<div class="card" id="delivery-log">
|
|
<h2>Delivery log</h2>
|
|
<p class="muted">The lines Postfix wrote about this message in
|
|
<code>mail.log</code>, oldest first — the connection to the receiving server,
|
|
its reply, and the status that reply was filed as. Only lines carrying this
|
|
message's queue id are shown; the whole log is on the
|
|
<a href="/server/log">System log</a> page.</p>
|
|
|
|
{{if .LogRows}}
|
|
{{/* Two columns, the way the send log itself is a table: the times down one
|
|
edge are what the message's pace is read off — the seconds between the
|
|
connection and the reply — and lining them up is what makes that
|
|
readable. A line whose head was not a timestamp keeps its whole text in
|
|
the second column and leaves the first empty. */}}
|
|
<table class="log">
|
|
<thead>
|
|
<tr><th>Time</th><th>Message</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .LogRows}}
|
|
<tr>
|
|
<td class="time muted">{{if .Time}}{{.Time}}{{else}}—{{end}}</td>
|
|
<td class="log-text">{{.Text}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<p class="muted">{{.LogNote}}</p>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|