log panes: lines wrap under their start instead of scrolling sideways

The owner, 2026-10-16, a phone screenshot of a message's Delivery log:
"На телефоне при просмотре лога, появляются две полосы. Мы это можем
поправить?" - the horizontal scroll bars Chrome on Android drew over a
pre whose mail.log lines were wider than it. As the mockup of 216f366:
sp-log wraps (pre-wrap, overflow-wrap: anywhere) and no longer scrolls;
log_pane writes each line as a block, sp-ll, with a 4ch hanging indent and
its newline inside it, so copied text keeps one line per line. One
partial: the Delivery log of a message, System log, the outbound queue's
listing.

Tests: TestLogPaneIsAPreWithEscapedLines reads the new shape; the page
tests' substrings stand; TestPanelCSSIsTheMockupStylesheet holds panel.css
to the mockup. go vet + go test (26 packages) and make e2e on the test
server for this tree. CHANGELOG [Unreleased] § Fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-16 14:31:17 +03:00
co-authored by Claude Opus 5.5
parent 216f366dfa
commit bd21ff0f90
4 changed files with 22 additions and 8 deletions
+8
View File
@@ -5,6 +5,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
## [Unreleased] ## [Unreleased]
### Fixed
- **Log panes wrap their lines instead of scrolling sideways**: a phone drew
two scroll bars over the Delivery log of a message, and the end of a
delivery line - `status=` and the reply - was out of sight. A long line
wraps, the rest indented under its start; copied text keeps one line per
line. The Delivery log, System log and the queue's listing.
## [2.9.0] - 2026-10-16 ## [2.9.0] - 2026-10-16
The panel's DNS records read faster: a record DNS already holds as shown The panel's DNS records read faster: a record DNS already holds as shown
+7 -5
View File
@@ -364,17 +364,19 @@ func TestTimelineLevels(t *testing.T) {
`<li class="sp-pending"><time>next attempt</time>Still queued</li>`) `<li class="sp-pending"><time>next attempt</time>Still queued</li>`)
} }
// The log is a pre: its lines are separated by a newline of their own, not by // The log is a pre of lines: each a block of its own (sp-ll), which wraps
// the template's layout whitespace, and a line is escaped. // under its start rather than scrolling sideways, ended by a newline inside it
// so copied text keeps one line per line - and nothing of the template's
// layout whitespace between them. A line is escaped.
func TestLogPaneIsAPreWithEscapedLines(t *testing.T) { func TestLogPaneIsAPreWithEscapedLines(t *testing.T) {
out := partial(t, "log_pane", []LogLine{ out := partial(t, "log_pane", []LogLine{
{Time: "13:58:07", Text: "message-id=<a81f0c@alertmanager>"}, {Time: "13:58:07", Text: "message-id=<a81f0c@alertmanager>"},
{Time: "13:58:09", Text: "status=deferred", Level: "warn"}, {Time: "13:58:09", Text: "status=deferred", Level: "warn"},
{Text: "no time on this one", Level: "error"}, {Text: "no time on this one", Level: "error"},
}) })
want := `<pre class="sp-log"><span class="sp-d">13:58:07</span> message-id=&lt;a81f0c@alertmanager&gt;` + "\n" + want := `<pre class="sp-log"><span class="sp-ll"><span class="sp-d">13:58:07</span> message-id=&lt;a81f0c@alertmanager&gt;` + "\n</span>" +
`<span class="sp-d">13:58:09</span> <span class="sp-w">status=deferred</span>` + "\n" + `<span class="sp-ll"><span class="sp-d">13:58:09</span> <span class="sp-w">status=deferred</span>` + "\n</span>" +
`<span class="sp-e">no time on this one</span></pre>` `<span class="sp-ll"><span class="sp-e">no time on this one</span>` + "\n</span></pre>"
if out != want { if out != want {
t.Errorf("log_pane =\n%s\nwant\n%s", out, want) t.Errorf("log_pane =\n%s\nwant\n%s", out, want)
} }
+5 -1
View File
@@ -295,8 +295,12 @@ details.sp-record[open] .sp-record-toggle span + span { display: inline; }
/* ---- logs, timeline, help */ /* ---- logs, timeline, help */
.sp-log { .sp-log {
margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0; margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0;
font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; overflow-x: auto; white-space: pre; font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere;
} }
/* One line of a log: long ones wrap inside the pane, the rest of the line
indented under its start, so nothing scrolls sideways - a phone drew the
scroll bars over the text (owner, 2026-10-16). */
.sp-log .sp-ll { display: block; padding-left: 4ch; text-indent: -4ch; }
.sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; } .sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; }
.sp-timeline { list-style: none; margin: 0; padding: 0; } .sp-timeline { list-style: none; margin: 0; padding: 0; }
.sp-timeline li { position: relative; padding: 0 0 1.1rem 1.6rem; border-left: 2px solid var(--sp-rule); margin-left: .4rem; } .sp-timeline li { position: relative; padding: 0 0 1.1rem 1.6rem; border-left: 2px solid var(--sp-rule); margin-left: .4rem; }
+2 -2
View File
@@ -190,8 +190,8 @@
{{/* log_pane is a dark block of log lines (input: []LogLine). The time is {{/* log_pane is a dark block of log lines (input: []LogLine). The time is
dimmed, a warning amber, an error red. The newline between lines is part of dimmed, a warning amber, an error red. The newline between lines is part of
the text: the pane is a pre. */}} the text: the pane is a pre. */}}
{{define "log_pane"}}<pre class="sp-log">{{range $i, $l := .}}{{if $i}} {{define "log_pane"}}<pre class="sp-log">{{range .}}<span class="sp-ll">{{if .Time}}<span class="sp-d">{{.Time}}</span> {{end}}{{if eq .Level "warn"}}<span class="sp-w">{{.Text}}</span>{{else if eq .Level "error"}}<span class="sp-e">{{.Text}}</span>{{else}}{{.Text}}{{end}}
{{end}}{{if $l.Time}}<span class="sp-d">{{$l.Time}}</span> {{end}}{{if eq $l.Level "warn"}}<span class="sp-w">{{$l.Text}}</span>{{else if eq $l.Level "error"}}<span class="sp-e">{{$l.Text}}</span>{{else}}{{$l.Text}}{{end}}{{end}}</pre>{{end}} </span>{{end}}</pre>{{end}}
{{/* empty_state says what is absent and why that is fine (input: EmptyState). */}} {{/* empty_state says what is absent and why that is fine (input: EmptyState). */}}
{{define "empty_state"}}<div class="sp-empty"><i class="ti {{.Icon}}"></i>{{template "inline" .Text}}</div>{{end}} {{define "empty_state"}}<div class="sp-empty"><i class="ti {{.Icon}}"></i>{{template "inline" .Text}}</div>{{end}}