Files
selfpost/internal/web/view/templates/layout.html
T
mixandClaude Opus 5.5 6db3704711 queue-table: Outbound › Queue as the drawn table, Inbound › Queue built
Owner, 2026-10-18: "Фиаско, что очередь не была сделана. Как мы это
допустили? Исправляем!" The mockups out-queue and in-queue (design 629de5e,
guards 8525e8e).

- postfix: postqueue -j gives each message's size and each recipient's
  delay_reason (src/postqueue/showq_json.c, format_json(): "delay_reason"
  only when LEN(why) > 0); QueueListing reads it with the page's 5 s,
  QueueMessages keeps the notifier's 30 s.
- view: one Queue page type for both directions (queue.go), one polled box
  (queue_body.html): a row per recipient, oldest first, cut at 200 with a
  foot; the head "N messages · size", "Refreshes on its own" while empty;
  Last reply is the delay reason, or where the message is (active, hold,
  incoming) before there was one. Inbound has no Details: an inbound
  message has no page of its own.
- handlers: one read of the queue, split by the recipient's domain against
  the inbound domains; an outbound recipient is linked to its send-log row
  (store.SendLogRefsByQueueID, through idx_send_log_queue_id, plan pinned in
  TestSendLogQueriesUseTheirIndexes), matched without regard to case, else
  the message's first row. Both pages are the global role's; the inbound
  one 404s with the relay off. Routes /inbound/queue (+ /fragment), menu,
  touch menu and sibling strip.
- Overview: Queues · Both empty · outbound and inbound with the relay on.
- Help: the queue topic rewritten, an Inbound queue topic (word for word with
  the mockup; domain administrators do not see it, as with the lists).
- TestUnbuiltScreensAreOnTheRoadmap and its rule in development.md: an
  exempted screen must be named in roadmap.md; queue-table leaves the
  roadmap; guide, architecture, panel-redesign.md updated.

Tests: go vet; go test ./internal/web/... ./internal/store ./internal/postfix
./internal/notify pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:17:14 +03:00

174 lines
15 KiB
HTML

{{/* The shell of every redesigned page (docs/plans/panel-redesign.md § Three
layers): a brick navbar with the wordmark and hover menus, the stamp's
perforated edge, the strip of sibling pages, the page, the footer. It is
built exactly like the mockups' build.py builds it, and it is the only
place besides components.html where sp- classes are written. A page never
writes any of it.
layout.html is executed with a Frame (components.go): .Shell is what the
shell shows, .Page is the page's own data, handed to the page's "content"
template. Which entries are visible comes from .Shell (Engine.shell derives
it from the role, the feature flags and the user's reach); the paths are
written out here, so the menu can be read, and checked against the router
(guard_routes_test.go), without running anything. */}}
{{/* layout_head is the head of both documents; the stylesheets, in this order,
and the two scripts are all a page ever loads. */}}
{{define "layout_head"}}<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · SelfPost</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 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. HTML links.html#rel-icon leaves "most appropriate" to the
browser, so measured, 2026-10-15, by which file was requested: Edge 155
(Chromium) takes the SVG, the Claude desktop app's embedded browser
(Chromium 152) the PNG, and adding sizes="any" to the SVG changes
neither. Firefox and Safari not measured (assumptions D-B16). */}}
<link rel="icon" href="/static/favicon.png" sizes="32x32">
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/bulma.min.css">
<link rel="stylesheet" href="/static/tabler-icons.css">
<link rel="stylesheet" href="/static/panel.css">
{{/* Unless told otherwise, htmx injects a stylesheet element of its own into
the head for the request-indicator classes. The panel uses no
hx-indicator, and that injected element would be the one thing the
Content-Security-Policy has to make an exception for, so it
is switched off here. htmx 2.0.4, src/htmx.js: the ready handler runs
mergeMetaConfig() before insertIndicatorStyles(), which adds the element
only while includeIndicatorStyles is not false. Both scripts are defer:
they run in this order once parsing ends and before DOMContentLoaded
(HTML Living Standard, parsing.html#the-end). */}}
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
<script src="/static/htmx.min.js" defer></script>
<script src="/static/panel.js" defer></script>
</head>{{end}}
{{/* layout.html is the signed-in document. Engine.Render uses
layout_signed_out instead when there is no user. */}}
{{define "layout.html"}}<!DOCTYPE html>
<html lang="en" data-theme="light">
{{template "layout_head" .Shell}}
<body>
{{template "nav" .Shell}}
{{template "subnav" .Shell}}
<main>
<div class="container sp-page">
{{template "content" .Page}}
{{/* Appropriate Legal Notices (AGPL-3.0): copyright, how to read the
licence, where the Corresponding Source is, and that there is no
warranty. The running version is shown signed in only: it is what a
backup manifest is checked against on restore, and must not be
advertised to the internet. */}}
<footer class="sp-foot">SelfPost {{.Shell.Version}} · {{.Shell.Copyright}} · <a href="/license">License (AGPL-3.0)</a> · <a href="{{.Shell.SourceURL}}">Source</a> · No warranty</footer>
</div>
</main>
</body>
</html>{{end}}
{{/* layout_signed_out is the split brand / form screen of sign-in and setup:
the stamp on the brick, the page's own form beside it, and the legal
notice without the version. */}}
{{define "layout_signed_out"}}<!DOCTYPE html>
<html lang="en" data-theme="light">
{{template "layout_head" .Shell}}
<body>
<div class="columns is-gapless sp-signin">
<div class="column is-half sp-signin-brand">
<div>
<img src="/static/logo.svg" alt="SelfPost">
<p>YOUR SMTP. YOUR RULES.</p>
</div>
</div>
<div class="column is-half sp-signin-form">
{{template "content" .Page}}
<p class="sp-foot">{{.Shell.Copyright}} · <a href="/license">License (AGPL-3.0)</a> · <a href="{{.Shell.SourceURL}}">Source</a> · No warranty</p>
</div>
</div>
</body>
</html>{{end}}
{{/* nav is the navbar and the perforated edge under it (input: Shell). A group
with nothing in it for this user is absent; the group of the current page
carries sp-current and its current entry is-active. The bar's menu shows
from 1024 px (Bulma hides a navbar-menu without is-active below it);
below, touchnav takes its place. */}}
{{define "nav"}}<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="{{.Home}}"><img src="/static/wordmark.svg" alt="SelfPost"></a>{{template "touchnav" .}}</div>
<div class="navbar-menu">
<div class="navbar-start">
{{if .ShowOverview}} <a class="navbar-item{{if eq .Section "overview"}} sp-current{{end}}" href="/overview">Overview</a>
{{end}}{{if .ShowOutbound}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "outbound"}} sp-current{{end}}">
<a class="navbar-link" href="/outbound/domains">Outbound</a>
<div class="navbar-dropdown">
<a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "domains")}} is-active{{end}}" href="/outbound/domains"><i class="ti ti-world-upload"></i>Domains</a>
<a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "log")}} is-active{{end}}" href="/outbound/log"><i class="ti ti-list-details"></i>Log</a>
{{if .ShowQueue}} <a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "queue")}} is-active{{end}}" href="/outbound/queue"><i class="ti ti-stack-2"></i>Queue</a>
{{end}}{{if .ShowDMARC}} <a class="navbar-item{{if and (eq .Section "outbound") (eq .Page "dmarc")}} is-active{{end}}" href="/outbound/dmarc"><i class="ti ti-report-analytics"></i>DMARC reports</a>
{{end}} </div>
</div>
{{end}}{{if .ShowInbound}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "inbound"}} sp-current{{end}}">
<a class="navbar-link" href="/inbound/domains">Inbound</a>
<div class="navbar-dropdown">
<a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "domains")}} is-active{{end}}" href="/inbound/domains"><i class="ti ti-world-download"></i>Domains</a>
<a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "log")}} is-active{{end}}" href="/inbound/log"><i class="ti ti-list-details"></i>Log</a>
{{if .ShowQueue}} <a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "queue")}} is-active{{end}}" href="/inbound/queue"><i class="ti ti-stack-2"></i>Queue</a>
{{end}}{{if .ShowFilterLists}} <a class="navbar-item{{if and (eq .Section "inbound") (eq .Page "filter-lists")}} is-active{{end}}" href="/inbound/filter-lists"><i class="ti ti-filter"></i>Filter lists</a>
{{end}} </div>
</div>
{{end}}{{if .ShowServer}} <div class="navbar-item has-dropdown is-hoverable{{if eq .Section "server"}} sp-current{{end}}">
<a class="navbar-link" href="/server/health">Server</a>
<div class="navbar-dropdown">
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "health")}} is-active{{end}}" href="/server/health"><i class="ti ti-heartbeat"></i>Health</a>
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "log")}} is-active{{end}}" href="/server/log"><i class="ti ti-file-text"></i>System log</a>
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "backup")}} is-active{{end}}" href="/server/backup"><i class="ti ti-archive"></i>Backup</a>
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "users")}} is-active{{end}}" href="/server/users"><i class="ti ti-users"></i>Users</a>
<a class="navbar-item{{if and (eq .Section "server") (eq .Page "settings")}} is-active{{end}}" href="/server/settings"><i class="ti ti-settings"></i>Settings</a>
</div>
</div>
{{end}} </div>
<div class="navbar-end">
<div class="navbar-item has-dropdown is-hoverable{{if eq .Section "user"}} sp-current{{end}}">
<a class="navbar-link" href="/account"><i class="ti ti-user-circle"></i>{{.User}}</a>
<div class="navbar-dropdown is-right">
<a class="navbar-item" href="/account"><i class="ti ti-key"></i>Account</a>
<a class="navbar-item" href="/help"><i class="ti ti-help"></i>Help</a>
<hr class="navbar-divider">
{{/* Signing out is a POST (the handler answers nothing else), so it
stays a form that works without scripts. The submit button
carries navbar-item: Bulma resets a button to plain text, so
it takes the dropdown's entry styling like the links around
it, and panel.css hands it the hover tint Bulma gives to
a.navbar-item alone. */}}
<form method="post" action="/logout"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
</div>
</div>
</div>
</div>
</div>
</nav>
<div class="sp-perf" aria-hidden="true"></div>{{end}}
{{/* touchnav is the navigation below 1024 px (input: Shell; mockup
build.py, touchnav()): Menu opens a panel over the page with the same
groups as the bar, each a fold - the current one open - and the user's
last. <details> all through, so it works without a script. */}}
{{define "touchnav"}}<details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel">
{{if .ShowOverview}}<a href="/overview"{{if eq .Section "overview"}} aria-current="page"{{end}}>Overview</a>
{{end}}{{if .ShowOutbound}}<details class="sp-touchnav-group"{{if eq .Section "outbound"}} open{{end}}><summary>Outbound</summary><a href="/outbound/domains"{{if and (eq .Section "outbound") (eq .Page "domains")}} aria-current="page"{{end}}><i class="ti ti-world-upload"></i>Domains</a><a href="/outbound/log"{{if and (eq .Section "outbound") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/outbound/queue"{{if and (eq .Section "outbound") (eq .Page "queue")}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowDMARC}}<a href="/outbound/dmarc"{{if and (eq .Section "outbound") (eq .Page "dmarc")}} aria-current="page"{{end}}><i class="ti ti-report-analytics"></i>DMARC reports</a>{{end}}</details>
{{end}}{{if .ShowInbound}}<details class="sp-touchnav-group"{{if eq .Section "inbound"}} open{{end}}><summary>Inbound</summary><a href="/inbound/domains"{{if and (eq .Section "inbound") (eq .Page "domains")}} aria-current="page"{{end}}><i class="ti ti-world-download"></i>Domains</a><a href="/inbound/log"{{if and (eq .Section "inbound") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/inbound/queue"{{if and (eq .Section "inbound") (eq .Page "queue")}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowFilterLists}}<a href="/inbound/filter-lists"{{if and (eq .Section "inbound") (eq .Page "filter-lists")}} aria-current="page"{{end}}><i class="ti ti-filter"></i>Filter lists</a>{{end}}</details>
{{end}}{{if .ShowServer}}<details class="sp-touchnav-group"{{if eq .Section "server"}} open{{end}}><summary>Server</summary><a href="/server/health"{{if and (eq .Section "server") (eq .Page "health")}} aria-current="page"{{end}}><i class="ti ti-heartbeat"></i>Health</a><a href="/server/log"{{if and (eq .Section "server") (eq .Page "log")}} aria-current="page"{{end}}><i class="ti ti-file-text"></i>System log</a><a href="/server/backup"{{if and (eq .Section "server") (eq .Page "backup")}} aria-current="page"{{end}}><i class="ti ti-archive"></i>Backup</a><a href="/server/users"{{if and (eq .Section "server") (eq .Page "users")}} aria-current="page"{{end}}><i class="ti ti-users"></i>Users</a><a href="/server/settings"{{if and (eq .Section "server") (eq .Page "settings")}} aria-current="page"{{end}}><i class="ti ti-settings"></i>Settings</a></details>
{{end}}<details class="sp-touchnav-group"{{if eq .Section "user"}} open{{end}}><summary><i class="ti ti-user-circle"></i>{{.User}}</summary><a href="/account"><i class="ti ti-key"></i>Account</a><a href="/help"><i class="ti ti-help"></i>Help</a><form method="post" action="/logout"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details>
</div></details>{{end}}
{{/* subnav is the strip of sibling pages under the edge (input: Shell): the
pages of the current group, the current one marked. A page outside every
group (the user menu, the kit page's neighbours) has none. */}}
{{define "subnav"}}{{if and .ShowOutbound (eq .Section "outbound")}}<div class="sp-subnav"><div class="container"><a href="/outbound/domains"{{if eq .Page "domains"}} aria-current="page"{{end}}><i class="ti ti-world-upload"></i>Domains</a><a href="/outbound/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/outbound/queue"{{if eq .Page "queue"}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowDMARC}}<a href="/outbound/dmarc"{{if eq .Page "dmarc"}} aria-current="page"{{end}}><i class="ti ti-report-analytics"></i>DMARC reports</a>{{end}}</div></div>
{{else if and .ShowInbound (eq .Section "inbound")}}<div class="sp-subnav"><div class="container"><a href="/inbound/domains"{{if eq .Page "domains"}} aria-current="page"{{end}}><i class="ti ti-world-download"></i>Domains</a><a href="/inbound/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-list-details"></i>Log</a>{{if .ShowQueue}}<a href="/inbound/queue"{{if eq .Page "queue"}} aria-current="page"{{end}}><i class="ti ti-stack-2"></i>Queue</a>{{end}}{{if .ShowFilterLists}}<a href="/inbound/filter-lists"{{if eq .Page "filter-lists"}} aria-current="page"{{end}}><i class="ti ti-filter"></i>Filter lists</a>{{end}}</div></div>
{{else if and .ShowServer (eq .Section "server")}}<div class="sp-subnav"><div class="container"><a href="/server/health"{{if eq .Page "health"}} aria-current="page"{{end}}><i class="ti ti-heartbeat"></i>Health</a><a href="/server/log"{{if eq .Page "log"}} aria-current="page"{{end}}><i class="ti ti-file-text"></i>System log</a><a href="/server/backup"{{if eq .Page "backup"}} aria-current="page"{{end}}><i class="ti ti-archive"></i>Backup</a><a href="/server/users"{{if eq .Page "users"}} aria-current="page"{{end}}><i class="ti ti-users"></i>Users</a><a href="/server/settings"{{if eq .Page "settings"}} aria-current="page"{{end}}><i class="ti ti-settings"></i>Settings</a></div></div>
{{end}}{{end}}