43841790d4
The lines came out as one block of preformatted text, which is what the system log page does with a tail of mail.log — right there, where the lines are unrelated to each other and the block is the log itself. Here they are one message's six or seven lines, and what is read off them is the pace: the second between the connection and the banner, the ten between DATA and the reply. Run together, every line began with a different-width stamp and none of those numbers lined up. So they are two columns now, when and what, the same shape the send log itself has. logtail.SplitTimestamp takes the stamp off the head of a line: postlogd's format, which is what this server writes, and syslog's traditional one for a deployment that routes the log through syslogd instead. The stamp loses its microseconds and its offset — five decimal places are the widest part of the column and the least worth reading — but is not converted, so the page shows the log's own wall clock rather than a claim about which zone it was in. A line whose head is not a stamp either parser recognises keeps its whole text in the second column and leaves the first empty. The format is the log's, not ours; a line we cannot split is a line we must not drop, and the test says so. Co-Authored-By: Claude Opus 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>
|
|
|
|
<a class="back" href="{{.BackURL}}">← Back to deliveries</a>
|
|
|
|
{{/* 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="/system-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}}
|