Files
selfpost/internal/web/view/templates/layout.html
T
mixandClaude Opus 5.5 ad3d834392 store, web: cite the source of what they rely on (assumptions part D)
Comments only, no behaviour changed. modernc.org/sqlite v1.60.1 (the
_pragma list on every connection, busy_timeout first; extended result
codes; a deferred BEGIN; multi-statement Exec) and SQLite 3.53.4's
documentation (transactions, WAL, foreign keys, NOCASE, upsert, julianday,
partial indexes); htmx 2.0.4 src/htmx.js; the Fetch and HTML standards for
Sec-Fetch-Site, Origin, SameSite, __Host-, CSP and defer order; Go's
html/template. Comments the source contradicts are corrected - the driver
does not serialise writes (SetMaxOpenConns(1) does), htmx:load fires per
swapped element, no-store does not keep a page out of history.

Left for their own commits: D-S3 a migration two processes can both run,
D-S20 recipient case, D-B3 Origin: null under Referrer-Policy: no-referrer,
D-H5/H6/H7/H9 (audit M11, stage 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:36:26 +03:00

155 lines
11 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. */}}
<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. */}}
{{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></div>
<div class="navbar-menu is-active">
<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 .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}}
{{/* 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 .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}}