feat(panel): navigation as a left column, with a section index on the long pages
The navigation was a bar across the top that did not fit on one row — six page entries and the session block against the panel's width — and had to be split into two, costing the top of every page. It is now a column down the left edge: one left edge to scan, the current entry marked down its leading edge, sticky so it stays in view, and room under the entries for the current page's own sections. Below the width the two columns need it lies back down into the same wrapping rows as before; six entries need no drawer. The section index is for the two pages long enough to need one — the domain page (nine cards) and the status page (eight). Each card carries an id and the page's template defines the list by overriding an empty "sections" block in the layout, so a page that defines nothing renders no index. panel.js marks the section in view, looking targets up by id on each pass so the status page swapping its cards out every five seconds cannot leave it measuring boxes that have left the document; the links themselves are plain fragment links and need no script. Verified against the real pages rendered by a local panel at 1300px, 924px and 481px wide.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
||||
|
||||
{{if .NewCred}}
|
||||
<div class="card credential">
|
||||
<div class="card credential" id="new-credential">
|
||||
<h2>New application password</h2>
|
||||
<p class="muted">This password is shown <strong>once only</strong> and is not
|
||||
stored. Copy it now — if it is lost, regenerate a new one.</p>
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="dkim">
|
||||
<h2>DKIM DNS record</h2>
|
||||
<p class="muted">Publish this TXT record in the DNS for <strong>{{.Domain.Name}}</strong>.
|
||||
It is not a secret and can be viewed at any time.</p>
|
||||
@@ -47,7 +47,7 @@
|
||||
<p class="muted">Mail is signed with selector <strong>{{.Domain.DKIMSelector}}</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="spf-dmarc">
|
||||
<h2>SPF and DMARC records</h2>
|
||||
<p class="muted">These two are not generated the way the DKIM record above is —
|
||||
they are policy, and the domain may already publish an SPF record for other
|
||||
@@ -91,7 +91,7 @@
|
||||
<code>p=reject</code> once the reports come back clean.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="dns-status">
|
||||
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
|
||||
<p class="muted">What DNS publishes for <strong>{{.Domain.Name}}</strong> right
|
||||
now, checked against the key this server signs with. Results are cached for a
|
||||
@@ -121,7 +121,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="settings">
|
||||
<h2>Sending server settings</h2>
|
||||
<p class="muted">Point the mail client or script at these settings and
|
||||
authenticate with an application login and password from the
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
{{/* Create form above the list, the same order the domains page uses for
|
||||
"Add a sending domain" above "Domains". */}}
|
||||
<div class="card">
|
||||
<div class="card" id="add-application">
|
||||
<h2>Add an application</h2>
|
||||
<form method="post" action="/domains/{{.Domain.ID}}/applications">
|
||||
<label for="login">Login</label>
|
||||
@@ -173,7 +173,7 @@
|
||||
be unique across all domains and may contain letters, digits, '.', '-' and '_'.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="applications">
|
||||
<h2>Applications</h2>
|
||||
<p class="muted">Each application is a SASL login/password an app or script
|
||||
uses to send mail as this domain. A login may send from any address of the
|
||||
@@ -241,7 +241,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="rate-limit">
|
||||
<h2>Sending rate limit (domain)</h2>
|
||||
<p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this
|
||||
domain may send from its expected client IP(s) within a time window, summed
|
||||
@@ -276,7 +276,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="export">
|
||||
<h2>Export domain</h2>
|
||||
<p class="muted">Download this domain to move it to another SelfPost instance:
|
||||
its DKIM key, selector and every application with its working password. On
|
||||
@@ -292,10 +292,33 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="danger">
|
||||
<h2>Danger zone</h2>
|
||||
<p class="muted">Deleting this domain also deletes its DKIM key and every
|
||||
application bound to it.</p>
|
||||
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* The domain page's section index, shown in the navigation column (see the
|
||||
"sections" block in layout.html). This is the panel's longest page — the
|
||||
DNS records to publish, the checks on them, the applications and two rate
|
||||
limits — and setting a domain up means going back and forth between them.
|
||||
The freshly generated password is only listed while it is on the page: it
|
||||
is the one card that is not always there, and the one nothing should scroll
|
||||
away from silently. */}}
|
||||
{{define "sections"}}
|
||||
<div class="sections">
|
||||
<p class="sections-title">On this page</p>
|
||||
{{if .NewCred}}<a href="#new-credential">New application password</a>{{end}}
|
||||
<a href="#dkim">DKIM DNS record</a>
|
||||
<a href="#spf-dmarc">SPF and DMARC records</a>
|
||||
<a href="#dns-status">DNS status</a>
|
||||
<a href="#settings">Sending server settings</a>
|
||||
<a href="#add-application">Add an application</a>
|
||||
<a href="#applications">Applications</a>
|
||||
<a href="#rate-limit">Sending rate limit</a>
|
||||
<a href="#export">Export domain</a>
|
||||
<a href="#danger">Danger zone</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
<script src="/static/panel.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
{{/* Two columns: the navigation on the left and the page beside it, centred
|
||||
together (see .shell in panel.css). The signed-out pages have no navigation
|
||||
and the shell holds their card alone. */}}
|
||||
<div class="shell">
|
||||
{{if .User}}{{template "nav" .}}{{end}}
|
||||
{{/* The page name travels onto <main> 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. */}}
|
||||
<main class="page-{{.Active}}">
|
||||
{{if .User}}{{template "nav" .}}{{end}}
|
||||
{{template "content" .}}
|
||||
{{/* The running version, on every authenticated page: it is what a backup
|
||||
manifest is checked against on restore and the first thing to establish
|
||||
@@ -36,6 +40,7 @@
|
||||
the login and setup pages must not advertise it to the internet. */}}
|
||||
{{if .User}}<footer class="version">SelfPost {{.Version}}</footer>{{end}}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
|
||||
@@ -46,21 +51,11 @@
|
||||
.User and get no nav. */}}
|
||||
{{define "nav"}}
|
||||
<nav class="nav">
|
||||
{{/* The top row is first here because it is drawn first (see .nav in
|
||||
panel.css), which keeps the tab order matching the visual one. It holds
|
||||
the mark on the left and the session block on the right; the page entries
|
||||
follow underneath. The mark goes to /status, the page the panel opens on —
|
||||
named outright rather than as "/", which is only a redirect to it. */}}
|
||||
<div class="top">
|
||||
<a class="brand" href="/status"><img src="/static/logo-compact.svg" width="110" height="50" alt="SelfPost"></a>
|
||||
<div class="session">
|
||||
<span class="muted">{{.User}}</span>
|
||||
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Account</span>{{else}}<a href="/account">{{template "icon-account"}}Account</a>{{end}}
|
||||
<form class="inline" method="post" action="/logout">
|
||||
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{/* The order of the blocks is the order they are read in, top to bottom of
|
||||
the column: the mark, the panel's pages, the current page's own sections,
|
||||
and the session last. The mark goes to /status, the page the panel opens
|
||||
on — named outright rather than as "/", which is only a redirect to it. */}}
|
||||
<a class="brand" href="/status"><img src="/static/logo-compact.svg" width="110" height="50" alt="SelfPost"></a>
|
||||
<div class="links">
|
||||
{{if eq .Active "status"}}<span aria-current="page">{{template "icon-status"}}Status</span>{{else}}<a href="/status">{{template "icon-status"}}Status</a>{{end}}
|
||||
{{if eq .Active "domains"}}<span aria-current="page">{{template "icon-domains"}}Domains</span>{{else}}<a href="/domains">{{template "icon-domains"}}Domains</a>{{end}}
|
||||
@@ -69,9 +64,27 @@
|
||||
{{if eq .Active "system_log"}}<span aria-current="page">{{template "icon-system-log"}}System log</span>{{else}}<a href="/system-log">{{template "icon-system-log"}}System log</a>{{end}}
|
||||
{{if eq .Active "backup"}}<span aria-current="page">{{template "icon-backup"}}Backup</span>{{else}}<a href="/backup">{{template "icon-backup"}}Backup</a>{{end}}
|
||||
</div>
|
||||
{{template "sections" .}}
|
||||
<div class="session">
|
||||
<span class="muted">{{.User}}</span>
|
||||
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Account</span>{{else}}<a href="/account">{{template "icon-account"}}Account</a>{{end}}
|
||||
<form class="inline" method="post" action="/logout">
|
||||
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
{{/* The index of the current page's own sections, shown in the navigation
|
||||
column under the page entries. Most pages are one or two cards and need no
|
||||
index, so the block is empty here and only the pages that are genuinely
|
||||
long — the domain page, the status page — redefine it with their own list
|
||||
(see .sections in panel.css for what it looks like, and panel.js for the
|
||||
marking of the section in view). Each page's template file is parsed after
|
||||
this one, so its definition replaces this empty one; a page that defines
|
||||
nothing keeps it and renders no index. */}}
|
||||
{{define "sections"}}{{end}}
|
||||
|
||||
{{/* Navigation icons. Inline SVG rather than an icon font or sprite file: they
|
||||
inherit the link's colour through currentColor, cost no extra request, and
|
||||
need no exemption from the panel's "default-src 'self'" policy. Each is
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{{template "status_body" .}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="hostname">
|
||||
<h2>Hostname and reverse DNS <span class="st st-{{.PTR.Status}}">{{.PTR.Status}}</span></h2>
|
||||
<p class="muted">Receiving servers check that the name this server announces
|
||||
resolves to its address <em>and</em> that the address resolves back to the same
|
||||
@@ -29,7 +29,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="configuration">
|
||||
<h2>Configuration</h2>
|
||||
<p class="muted">Regenerates the OpenDKIM and Postfix configuration from the
|
||||
database and reloads both daemons. Use it if you edited the files by hand,
|
||||
@@ -41,3 +41,22 @@
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* The status page's section index, shown in the navigation column (see the
|
||||
"sections" block in layout.html). The first six cards are the ones the
|
||||
polling fragment replaces every five seconds; their ids are part of
|
||||
status_body.html and do not change with the reading, so the links here hold
|
||||
across a refresh. */}}
|
||||
{{define "sections"}}
|
||||
<div class="sections">
|
||||
<p class="sections-title">On this page</p>
|
||||
<a href="#overall">Overall</a>
|
||||
<a href="#processes">Processes</a>
|
||||
<a href="#machine">Machine</a>
|
||||
<a href="#queue">Mail queue</a>
|
||||
<a href="#certificate">TLS certificate</a>
|
||||
<a href="#sockets">Milter sockets</a>
|
||||
<a href="#hostname">Hostname and reverse DNS</a>
|
||||
<a href="#configuration">Configuration</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{define "status_body"}}
|
||||
<div id="status-body" hx-get="/status/fragment" hx-trigger="every 5s" hx-swap="outerHTML">
|
||||
<div class="card">
|
||||
<div class="card" id="overall">
|
||||
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
||||
<p class="muted">{{.OverallHeading}}</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="processes">
|
||||
<h2>Processes <span class="st st-{{.ProcessStatus}}">{{.ProcessStatus}}</span></h2>
|
||||
{{if .ProcessError}}
|
||||
<p class="error">Could not ask supervisord for the process list.</p>
|
||||
@@ -25,7 +25,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="machine">
|
||||
<h2>Machine <span class="st st-{{.Machine.Status}}">{{.Machine.Status}}</span></h2>
|
||||
<p class="muted">Processor, memory and network of the machine this container
|
||||
runs on, read from the kernel's counters. CPU and throughput are rates, so
|
||||
@@ -72,7 +72,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="queue">
|
||||
<h2>Mail queue <span class="st st-{{.QueueStatus}}">{{.QueueStatus}}</span></h2>
|
||||
{{if .QueueError}}
|
||||
<p class="error">{{.QueueError}}</p>
|
||||
@@ -82,7 +82,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="certificate">
|
||||
<h2>TLS certificate <span class="st st-{{.Cert.Status}}">{{.Cert.Status}}</span></h2>
|
||||
<p class="muted">The certificate Postfix serves on port 465{{if .Cert.Subject}} ({{.Cert.Subject}}){{end}}.
|
||||
It is supplied by the reverse proxy through a read-only mount; SelfPost only reads it.</p>
|
||||
@@ -93,7 +93,7 @@
|
||||
<p class="{{if eq .Cert.Status "ok"}}muted{{else}}error{{end}}">{{.Cert.Detail}}</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="sockets">
|
||||
<h2>Milter sockets <span class="st st-{{.SocketStatus}}">{{.SocketStatus}}</span></h2>
|
||||
<table>
|
||||
<thead><tr><th>Milter</th><th>Socket</th><th>State</th></tr></thead>
|
||||
|
||||
Reference in New Issue
Block a user