diff --git a/CHANGELOG.md b/CHANGELOG.md index 9929f99..5c05525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,61 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +## [0.4.0] - 2026-08-04 + +### Added + +- The project's mark is now in use rather than only on file. The README opens + with the full stamp; the panel carries the compact one at the left of its + navigation bar, linking to the status page, and the full one above the card + on the two pages that have no navigation — sign-in and first-run setup. The + browser tab icon changes with it, from the earlier envelope drawing to the + stamp's small-size variant, so the tab, the panel and the README are one + identity. The four brand files in `docs/assets/` had their wordmark converted + from live text to outlines: they were set in IBM Plex Sans, which is not + installed on the machines that render them, and the light/semibold contrast + between *Self* and *Post* — the whole of the mark — collapsed into whatever + fallback the viewer happened to have. + +### Changed + +- panel: the sign-in and setup pages are now a column the width of their own + card. Both are a single narrow card, which centred itself while the heading + above it stayed at the panel's left edge; adding the mark would have made + that three alignments on a page with four elements. + +- panel: the three monitoring pages — Deliveries, Mail queue, System log — are + now laid out wider (64rem against the 48rem the rest of the panel keeps). + They carry data rather than prose: the send-log's seven columns had no room + to breathe, and the raw `mail.log` lines wrapped every second line. + +### Fixed + +- panel: Deliveries now shows the subject as text rather than as its MIME + encoding. A subject in any non-Latin alphabet reaches the milter as RFC 2047 + encoded-words (`=?utf-8?Q?=D0=9F…?=`), and the panel printed that verbatim — + unreadable, and as one unbreakable run wide enough to push the Status column + outside the card. Subjects are decoded when the message is journalled and + capped at 200 characters; the column clips anything still too long to one + line, with the full text in the tooltip. Rows logged before this release keep + their raw string. Subjects in the legacy single-byte charsets (windows-1251, + koi8-r) are still stored as sent — there is no decoder for them. + +- panel: table cells may now break inside a word, so no single long value can + push a table past the edge of its card. A 40-character recipient address did + it just as readily as an undecoded subject: a column is at least as wide as + the longest unbreakable run it holds, and email addresses have nothing to + break on. Timestamps are exempt and stay on one line. + +- panel: the Applications list on a domain page no longer comes apart. It was a + four-column table whose last column held six controls, two of them expanding + panels with textareas — far more than the width of a column, so the controls + broke into a staircase, the login cell grew into a block as tall as the row, + and the two text columns were left stranded on the baseline halfway down it. + An application is now a block rather than a row: the login on one line, mode + and addresses on the next, and the controls in a single wrapping row, with an + opened panel claiming the full width for its fields. + ## [0.3.0] - 2026-08-03 ### Fixed diff --git a/README.md b/README.md index cccca31..d09ddd3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![SelfPost](docs/assets/selfpost-stamp.svg) + # SelfPost Self-hosted outbound SMTP relay with a web control panel, shipped as a single diff --git a/docs/assets/selfpost-icon-16.svg b/docs/assets/selfpost-icon-16.svg index e6a77e7..9e714db 100644 --- a/docs/assets/selfpost-icon-16.svg +++ b/docs/assets/selfpost-icon-16.svg @@ -1,7 +1,8 @@ - - -SP - \ No newline at end of file + Wordmark set in IBM Plex Sans (OFL) and converted to outlines: the + file needs no font installed to render as drawn. --> + + + + diff --git a/docs/assets/selfpost-icon.svg b/docs/assets/selfpost-icon.svg index 365fee4..8b29d72 100644 --- a/docs/assets/selfpost-icon.svg +++ b/docs/assets/selfpost-icon.svg @@ -1,8 +1,9 @@ - - -Self -Post - \ No newline at end of file + Wordmark set in IBM Plex Sans (OFL) and converted to outlines: the + file needs no font installed to render as drawn. --> + + + + + diff --git a/docs/assets/selfpost-proof.html b/docs/assets/selfpost-proof.html index 11a688f..757dd0e 100644 --- a/docs/assets/selfpost-proof.html +++ b/docs/assets/selfpost-proof.html @@ -235,12 +235,12 @@

Файлы

- docs/brand/selfpost-stamp.svg — эталон, README, документация
- docs/brand/selfpost-stamp-compact.svg — шапка панели
- docs/brand/selfpost-icon.svg — аватар, фавикон от 32 px
- docs/brand/selfpost-icon-16.svg — фавикон 16–32 px + docs/assets/selfpost-stamp.svg — эталон, README, страницы входа панели
+ docs/assets/selfpost-stamp-compact.svg — шапка панели
+ docs/assets/selfpost-icon.svg — аватар, фавикон от 32 px
+ docs/assets/selfpost-icon-16.svg — фавикон 16–32 px
-
Осталось одно действие. В контейнере нет ни IBM Plex, ни сети, поэтому текст в файлах остался текстом. Перед коммитом его надо перевести в контуры, иначе на машине без Plex набор поедет:
inkscape --actions="select-all;object-to-path;export-filename:out.svg;export-do" in.svg
Пока в файлах прописан фолбэк 'IBM Plex Sans','Helvetica Neue',sans-serif — не спасёт вёрстку, но не даст ей развалиться совсем.
+
Контуры сделаны. Текст в четырёх файлах выше переведён в контуры по IBM Plex Sans (OFL) — шрифт на машине читателя больше не нужен, и Self с Post держат свои 200 и 600 везде. Панель отдаёт свои копии из internal/web/static/: logo.svg, logo-compact.svg, favicon.svg плюс растровый favicon.png 32×32 для браузеров без SVG-фавиконов — go:embed не дотягивается за пределы пакета, поэтому это копии, а не ссылки.
Макеты на этой странице остались с живым текстом: это протокол разбора знака, а не источник файлов.
\ No newline at end of file diff --git a/docs/assets/selfpost-stamp-compact.svg b/docs/assets/selfpost-stamp-compact.svg index 454ccb4..3c03928 100644 --- a/docs/assets/selfpost-stamp-compact.svg +++ b/docs/assets/selfpost-stamp-compact.svg @@ -1,7 +1,8 @@ - - -SelfPost - \ No newline at end of file + Wordmark set in IBM Plex Sans (OFL) and converted to outlines: the + file needs no font installed to render as drawn. --> + + + + diff --git a/docs/assets/selfpost-stamp.svg b/docs/assets/selfpost-stamp.svg index edc7ed1..cb46ba3 100644 --- a/docs/assets/selfpost-stamp.svg +++ b/docs/assets/selfpost-stamp.svg @@ -1,9 +1,10 @@ - - -SelfPost - -SELF-HOSTED SMTP RELAY - \ No newline at end of file + Wordmark set in IBM Plex Sans (OFL) and converted to outlines: the + file needs no font installed to render as drawn. --> + + + + + + diff --git a/internal/milter/milter.go b/internal/milter/milter.go index 5f34d81..cb58a48 100644 --- a/internal/milter/milter.go +++ b/internal/milter/milter.go @@ -13,6 +13,7 @@ package milter import ( "context" "log" + "mime" "net" "net/textproto" "strings" @@ -92,11 +93,35 @@ func (s *session) RcptTo(rcpt string, m *milter.Modifier) (milter.Response, erro // Header captures the Subject. Only the first Subject header is kept. func (s *session) Header(name, value string, m *milter.Modifier) (milter.Response, error) { if s.subject == "" && textproto.CanonicalMIMEHeaderKey(name) == "Subject" { - s.subject = value + s.subject = decodeSubject(value) } return milter.RespContinue, nil } +// subjectMaxRunes caps what the journal keeps of a subject. A Subject header +// may legally run to hundreds of characters; the log only needs enough to +// recognise the message, and the panel shows one row per recipient. +const subjectMaxRunes = 200 + +// decodeSubject turns the raw Subject header into display text. Anything +// non-ASCII arrives as RFC 2047 encoded-words (=?utf-8?Q?=D0=9F…?=), which the +// panel would otherwise show verbatim: unreadable, and — being one unbreakable +// run — wide enough to push the send-log table out of its card. Go's decoder +// covers the UTF-8 and ASCII charsets senders use in practice; for anything +// else (windows-1251, koi8-r) it fails and the raw header is kept, which is no +// worse than before. Truncation is applied after decoding so the cap counts +// characters of the subject, not bytes of its encoding. +func decodeSubject(v string) string { + if dec, err := (&mime.WordDecoder{}).DecodeHeader(v); err == nil { + v = dec + } + v = strings.TrimSpace(v) + if r := []rune(v); len(r) > subjectMaxRunes { + v = string(r[:subjectMaxRunes]) + "…" + } + return v +} + // Body fires at end-of-message, when the queue-id macro {i} is set and the // message is about to be committed to the queue. This is where the "queued" // rows are written. We accept (this milter is done) without ever rejecting. diff --git a/internal/milter/milter_test.go b/internal/milter/milter_test.go index 8b9bd41..999a43a 100644 --- a/internal/milter/milter_test.go +++ b/internal/milter/milter_test.go @@ -3,6 +3,7 @@ package milter import ( "errors" "net" + "strings" "testing" "time" @@ -113,6 +114,34 @@ func TestSessionRecordsRowPerRecipient(t *testing.T) { } } +// A subject in any non-ASCII alphabet reaches the milter as RFC 2047 +// encoded-words; the journal stores the text, not the encoding. +func TestHeaderDecodesEncodedSubject(t *testing.T) { + long := strings.Repeat("я", subjectMaxRunes+10) + + for _, tc := range []struct { + name, raw, want string + }{ + {"plain", "Hello there", "Hello there"}, + {"utf8 q", "=?utf-8?Q?=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0?=", "Проверка"}, + {"utf8 b, folded across two words", "=?utf-8?B?0J/RgNC40LLQtdGC?=\r\n =?utf-8?B?INC80LjRgA==?=", "Привет мир"}, + // No decoder for the legacy single-byte charsets: keep the header as + // sent rather than losing the subject entirely. + {"unknown charset", "=?windows-1251?B?z/Do4uXy?=", "=?windows-1251?B?z/Do4uXy?="}, + {"too long", long, strings.Repeat("я", subjectMaxRunes) + "…"}, + } { + t.Run(tc.name, func(t *testing.T) { + s := &session{rec: &fakeRecorder{}} + if _, err := s.Header("Subject", tc.raw, mods(nil)); err != nil { + t.Fatalf("Header: %v", err) + } + if s.subject != tc.want { + t.Fatalf("subject = %q, want %q", s.subject, tc.want) + } + }) + } +} + func TestBodyAcceptsEvenWhenRecorderFails(t *testing.T) { rec := &fakeRecorder{fail: true} s := &session{rec: rec} diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 0682e26..569c99a 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -98,6 +98,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { // a failing form. s.render(w, http.StatusOK, "login", map[string]any{ "Title": "SelfPost — Sign in", + "Active": "login", "SetupHint": true, }) return @@ -115,9 +116,13 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { } func (s *Server) renderLogin(w http.ResponseWriter, status int, formErr string) { + // Active names the page for the layout even though no navigation is drawn + // here: it is what puts page-login on
, which the stylesheet uses to + // give the signed-out pages a column the width of their own card. s.render(w, status, "login", map[string]any{ - "Title": "SelfPost — Sign in", - "Error": formErr, + "Title": "SelfPost — Sign in", + "Active": "login", + "Error": formErr, }) } diff --git a/internal/web/handlers_setup.go b/internal/web/handlers_setup.go index 020aeeb..1505f75 100644 --- a/internal/web/handlers_setup.go +++ b/internal/web/handlers_setup.go @@ -42,9 +42,10 @@ func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) { func (s *Server) renderSetupForm(w http.ResponseWriter, status int, token, formErr string) { s.render(w, status, "setup", map[string]any{ - "Title": "SelfPost — Create administrator", - "Token": token, - "Error": formErr, + "Title": "SelfPost — Create administrator", + "Active": "setup", + "Token": token, + "Error": formErr, }) } diff --git a/internal/web/static/favicon.png b/internal/web/static/favicon.png index aa319b8..cd7cc89 100644 Binary files a/internal/web/static/favicon.png and b/internal/web/static/favicon.png differ diff --git a/internal/web/static/favicon.svg b/internal/web/static/favicon.svg new file mode 100644 index 0000000..9e714db --- /dev/null +++ b/internal/web/static/favicon.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/internal/web/static/logo-compact.svg b/internal/web/static/logo-compact.svg new file mode 100644 index 0000000..3c03928 --- /dev/null +++ b/internal/web/static/logo-compact.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/internal/web/static/logo.svg b/internal/web/static/logo.svg new file mode 100644 index 0000000..cb46ba3 --- /dev/null +++ b/internal/web/static/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/internal/web/static/panel.css b/internal/web/static/panel.css index 5e4498b..2cede26 100644 --- a/internal/web/static/panel.css +++ b/internal/web/static/panel.css @@ -17,6 +17,13 @@ body { } main { max-width: 42rem; margin: 0 auto; } h1 { font-size: 1.4rem; margin: 0 0 1rem; } +/* The full mark, on the two pages that have no navigation bar to carry the + compact one. It takes the column's width so its edges line up with the card + below it, capped at that column's own 24rem; height stays automatic because + the stamp's proportions are part of the mark and it is never scaled unevenly. + The subtitle stops resolving below 280px, which a 320px viewport still clears + once the body's padding is taken off. */ +.mark { display: block; width: 100%; max-width: 24rem; height: auto; margin-bottom: 1.4rem; } .card { background: #fff; border: 1px solid #e2e5e9; border-radius: 10px; padding: 1.5rem; margin: 0 auto; @@ -50,14 +57,47 @@ button:hover, a.btn:hover { background: #1d4ed8; } form's block layout; its button is styled like any other. */ form.inline { display: inline; margin: 0; } main { max-width: 48rem; } +/* 48rem is a reading measure: right for the forms and prose that make up most + of the panel, and the width the navigation bar's two rows were sized for. The + three monitoring pages hold data instead — seven columns of send-log, and raw + mail.log lines that are long by nature — and at that width the send-log's + Subject and Status were fighting over the last inch while the log pages wrapped + every second line. They get a wider measure; every other page keeps the narrow + one. The class comes from the layout, which stamps the page name onto
. */ +main.page-deliveries, main.page-mail_queue, main.page-system_log { max-width: 64rem; } +/* The signed-out pages are a single card and nothing else, and .card.narrow + centres itself inside whatever holds it — so at the panel's usual width the + card floated in the middle while the mark and the heading stayed at the far + left, three alignments on a page with four elements. Narrowing the column to + the card's own width makes the three line up and puts the block as a whole in + the middle of the page. */ +main.page-login, main.page-setup { max-width: 24rem; } .card + .card { margin-top: 1.2rem; } .flash { background: #ecfdf3; border: 1px solid #abefc6; color: #067647; padding: 0.7rem 1rem; border-radius: 8px; margin-bottom: 1.2rem; } @media (prefers-color-scheme: dark) { .flash { background: #0d2818 !important; border-color: #1a5336 !important; color: #75d99b !important; } } table { width: 100%; border-collapse: collapse; } -th, td { text-align: left; padding: 0.5rem 0.4rem; border-bottom: 1px solid #e2e5e9; } +/* A table column is at least as wide as the longest unbreakable run inside it, + and the panel's tables are full of runs with nothing to break on: email + addresses, domains, queue ids. One 40-character recipient was enough to widen + the send-log past its card and hang Status over the edge. Cells may break mid + word, so a column can always be squeezed to the width available. */ +th, td { text-align: left; padding: 0.5rem 0.4rem; border-bottom: 1px solid #e2e5e9; overflow-wrap: anywhere; } +/* The exception: a timestamp broken across two lines is unreadable, and it is + short enough to never be the reason a row does not fit. */ +td.time { white-space: nowrap; } @media (prefers-color-scheme: dark) { th, td { border-color: #2b3138 !important; } } th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; color: #6b7280; } td.actions { text-align: right; } +/* Subject is the one cell whose text we do not control. Breaking mid word (the + rule above) keeps it inside the card, but a long subject would do it by + growing the row several lines tall, which buries the rows around it. So the + subject is clipped to one line instead, with the whole of it in the tooltip. + The clamp sits on an inner block box rather than the cell because max-width + on a is only advisory in the automatic table layout. */ +td.subject span { + display: block; max-width: 18rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} .code { display: block; white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem; background: #f0f2f4; border: 1px solid #e2e5e9; border-radius: 6px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0; } @media (prefers-color-scheme: dark) { .code { background: #14171a !important; border-color: #2b3138 !important; } } @@ -77,12 +117,28 @@ textarea { resize: vertical; } } button.danger, a.danger { background: #b42318; } button.danger:hover, a.danger:hover { background: #912018; } -td.actions form.inline, td.actions details { margin: 0.4rem 0 0 0.5rem; } /* The disclosure toggle is an action too, so it is drawn as a button (see the compact rule below); the marker is dropped because the pressed background already shows the open state. */ -td.actions summary { display: inline-block; list-style: none; cursor: pointer; } -td.actions summary::-webkit-details-marker { display: none; } +.actions summary { display: inline-block; list-style: none; cursor: pointer; } +.actions summary::-webkit-details-marker { display: none; } +/* Applications are a list of blocks, not table rows. As a table it fell apart: + four columns of which the last held six controls — two of them
+ panels with textareas — never fit the panel's 48rem. The controls wrapped + into a staircase, .code on the login cell grew into a slab as tall as the + row, and the two text cells sat on the baseline halfway down it. One block + per application gives the identity a line of its own and the controls a row + of their own, at the width they actually need. */ +.apps { list-style: none; margin: 1.2rem 0 0; padding: 0; } +.app { padding: 0.9rem 0; border-top: 1px solid #e2e5e9; } +.app:last-child { padding-bottom: 0; } +@media (prefers-color-scheme: dark) { .app { border-color: #2b3138 !important; } } +.app-login { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; } +.app-addr { margin: 0.15rem 0 0; word-break: break-all; } +.app .actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; } +/* An open panel claims a row to itself: its textareas and number inputs want + the block's full width, not the width of the summary that opened them. */ +.app .actions > details[open] { flex: 1 0 100%; } details form { margin-top: 0.6rem; } .credential { border-color: #f5c518; background: #fffbeb; } @media (prefers-color-scheme: dark) { .credential { background: #2a2408 !important; border-color: #6b5a10 !important; } } @@ -107,9 +163,21 @@ details form { margin-top: 0.6rem; } } @media (prefers-color-scheme: dark) { .nav { border-color: #2b3138 !important; } } .nav .links, .nav .session { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; align-items: center; } -/* Pushed to the far edge so the two rows read as two blocks rather than as one +/* The top row: mark at one edge, session block at the other. Splitting them to + the edges is what makes the two rows read as two blocks rather than as one ragged list that happened to wrap. */ +.nav .top { display: flex; align-items: center; justify-content: space-between; gap: 0.9rem; } .nav .session { justify-content: flex-end; } +/* The mark is a link, but not one of the bar's entries: it takes none of the + padding and rounding the entry rule below applies, so its own edge lines up + with the page rather than sitting half a step inside it. The stamp carries + about 4px of field inside the file at this size, which is what puts it level + with the icons of the row underneath. */ +.nav .brand { padding: 0; } +/* width/height are on the element too, so the row reserves the space before the + SVG has loaded; these keep the ratio if the box is ever squeezed. The stamp's + proportions are part of the mark — it is never scaled unevenly. */ +.nav .brand img { display: block; width: 110px; height: auto; } /* Each entry pairs an icon with its label, so the entry itself is a flex row rather than a run of text — that is also why the bar centres its items instead of aligning them on the text baseline. Account is included: it is a @@ -152,30 +220,32 @@ details form { margin-top: 0.6rem; } .code-row .code { flex: 1; min-width: 0; } /* Compact outlined button: same affordance as the filled one but quiet enough that several can sit together without shouting — the Copy buttons beside a - value, the per-application actions in a table row. Sign out overrides this - with .danger below since signing out is a deliberate, singular action. */ -button.copy, td.actions button, td.actions summary, td.actions a.danger, .nav button { + value, the controls of a table row or of an application block. Sign out + overrides this with .danger below since signing out is a deliberate, + singular action. .actions is the shared hook: a cell that holds controls, or + the control row of an application. */ +button.copy, .actions button, .actions summary, .actions a.danger, .nav button { margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600; border-radius: 6px; white-space: nowrap; background: #eef1f5; color: #2563eb; border: 1px solid #cfd4da; } -button.copy:hover, td.actions button:hover, td.actions summary:hover, -td.actions a.danger:hover, .nav button:hover { background: #e2e7ee; } +button.copy:hover, .actions button:hover, .actions summary:hover, +.actions a.danger:hover, .nav button:hover { background: #e2e7ee; } button.copy { flex: none; margin-top: 0.3rem; } -td.actions details[open] > summary { background: #dde3ec; } -td.actions button.danger, td.actions a.danger, .nav button.danger { +.actions details[open] > summary { background: #dde3ec; } +.actions button.danger, .actions a.danger, .nav button.danger { color: #b42318; background: #fef3f2; border-color: #fecdca; } -td.actions button.danger:hover, td.actions a.danger:hover, .nav button.danger:hover { background: #fee4e2; } +.actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: #fee4e2; } @media (prefers-color-scheme: dark) { - button.copy, td.actions button, td.actions summary, td.actions a.danger, .nav button { + button.copy, .actions button, .actions summary, .actions a.danger, .nav button { background: #22262b !important; border-color: #2b3138 !important; } - button.copy:hover, td.actions button:hover, td.actions summary:hover, - td.actions a.danger:hover, .nav button:hover { background: #2b3138 !important; } - td.actions details[open] > summary { background: #313841 !important; } - td.actions button.danger, td.actions a.danger, .nav button.danger { + button.copy:hover, .actions button:hover, .actions summary:hover, + .actions a.danger:hover, .nav button:hover { background: #2b3138 !important; } + .actions details[open] > summary { background: #313841 !important; } + .actions button.danger, .actions a.danger, .nav button.danger { color: #f5a29b !important; background: #2d1211 !important; border-color: #6b201a !important; } - td.actions button.danger:hover, td.actions a.danger:hover, .nav button.danger:hover { background: #3d1a18 !important; } + .actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: #3d1a18 !important; } } diff --git a/internal/web/templates/deliveries_rows.html b/internal/web/templates/deliveries_rows.html index a7f5455..8aa6024 100644 --- a/internal/web/templates/deliveries_rows.html +++ b/internal/web/templates/deliveries_rows.html @@ -9,12 +9,12 @@ {{range .Rows}} - {{.CreatedAt.Format "2006-01-02 15:04:05"}} + {{.CreatedAt.Format "2006-01-02 15:04:05"}} {{.Domain}} {{.AppLogin}} {{.From}} {{.To}} - {{.Subject}} + {{.Subject}} {{.Status}} {{end}} diff --git a/internal/web/templates/domain_detail.html b/internal/web/templates/domain_detail.html index da04ef8..879102a 100644 --- a/internal/web/templates/domain_detail.html +++ b/internal/web/templates/domain_detail.html @@ -180,21 +180,15 @@ domain (wildcard) or only from a fixed list of addresses.

{{if .Apps}} - - - - - - {{range .Apps}} - - - - - - - {{end}} - -
LoginModeAddresses
{{.Login}}{{if eq .AddressMode $.Wildcard}}Any address (@{{$.Domain.Name}}){{else}}List{{end}} - {{if eq .AddressMode $.Wildcard}}*@{{$.Domain.Name}}{{else}} - {{range $i, $a := .Addresses}}{{if $i}}, {{end}}{{$a}}{{end}} - {{end}} - +
    + {{range .Apps}} +
  • + +

    + {{if eq .AddressMode $.Wildcard}}Any address of the domain — *@{{$.Domain.Name}} + {{else}}Fixed list — {{range $i, $a := .Addresses}}{{if $i}}, {{end}}{{$a}}{{end}}{{end}} +

    +
    Edit mode
    @@ -238,11 +232,10 @@ data-confirm="Delete application {{.Login}}? Its credentials stop working immediately.">
    -
+ + + {{end}} + {{else}}

No applications yet. Add one above to get started.

{{end}} diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index d4ec6d2..5b3249c 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -4,7 +4,13 @@ {{.Title}} - +{{/* The tab icon is the stamp's small-size variant — the initials rather than + the two-line wordmark, which stops resolving below 32px (the thresholds are + recorded in docs/assets/selfpost-proof.html). The PNG is declared first and + the SVG second: a browser takes the last icon whose type it understands, so + vector wins wherever it is supported and the raster catches the rest. */}} + + {{/* Unless told otherwise, htmx injects a stylesheet element of its own into the head for the request-indicator classes. The panel uses no @@ -16,7 +22,12 @@ -
+{{/* The page name travels onto
as a class so the stylesheet can size a + page to what it holds — the monitoring pages are wider than the panel's + form pages (see main.page-* in panel.css). It is .Active, already carried + for the navigation, rather than a second field every handler would have to + remember to set. */}} +
{{if .User}}{{template "nav" .}}{{end}} {{template "content" .}} {{/* The running version, on every authenticated page: it is what a backup @@ -35,15 +46,20 @@ .User and get no nav. */}} {{define "nav"}}