Stage 2, step 6. Written by Sonnet, reviewed by Opus.
- Six pages named after their mockups: system-log, backup, users,
user, user-delete, help, with typed page data (view/server.go,
help.go), fixtures and tests. The old templates of the six are gone
(user_form.html, encrypt_fields.html deleted, the others rewritten)
and their names are off legacy_pages.txt, which now holds mail_queue
alone.
- Help is six help_topic blocks (checks, certificate, rdns, dns, apps,
limits); a test renders every fixture page and fails on a Help link
without a topic. The help drawer (help_drawer.html and its includes
in layout_legacy.html) is removed; its rules in legacy.css go with
the next step.
- The user form sets the user's e-mail (plan, Account e-mail): create
and update, validated, and a changed address resyncs DMARC for a
user whose default is the account e-mail - through the helper the
Account profile now shares.
- Handlers pass typed data; routes, POST targets, field names,
validation and authorization are unchanged. A test loads the user
form for four reach combinations and for the only global user, posts
it back untouched and finds the user unchanged.
Fixed on the way
- The user delete confirmation said the domains were not affected. A
domain that follows the user's default report address is left with
none; the page names those domains now.
- A refused backup lost the encryption choice and printed a literal
<code> element in its message.
Differences from the mockups, for the owner:
- Help has no DMARC and no Inbound topic: its outline fixes two boxes
of three topics, although other mockups link to #dmarc and #inbound.
Until the owner decides, Account's Help link goes to #dns and the
inbound MX box has none;
- system log: newest line first, as drawn; "refreshes on its own" (5 s
active, 30 s idle); only status=, warning: and error: lines are
coloured;
- backup: the file field is a plain file input; the encryption box is
ticked, as drawn and as on the domain export, so a plain download
means unticking it first;
- users: an E-mail column; the flash does not name the user;
- user form: no lead; the domain lists are no longer hidden by script
when Global is chosen (the server ignores them for that role).
Evidence
- docs/assets/panel-redesign/evidence/system-log.png, backup.png,
users.png, user.png, user-delete.png, help.png: 1440 px wide,
headless Edge, real templates served locally with their fixtures -
NOT from the container (no Docker on this machine).
- panel/audit.js on the six pages at 1440 and 1280: 720 texts checked,
problems: [].
- The system log's polling swap was not exercised in a browser; it
uses the mechanism of Overview and Health.
- go test -count=1 ./internal/web/... ./cmd/panel
ok internal/web
ok internal/web/auth
ok internal/web/handlers
ok internal/web/validate
ok internal/web/view
ok cmd/panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
14 KiB
HTML
159 lines
14 KiB
HTML
{{/* The layout of the pre-redesign pages: the old stylesheet (legacy.css) and the
|
|
side navigation. A page that has been restyled is rendered by layout.html
|
|
instead; Engine decides by the page's name (kitPages in view.go) and the
|
|
pair goes when the last old page does. */}}
|
|
{{define "layout_legacy.html"}}<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{.Title}}</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. */}}
|
|
<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/legacy.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. */}}
|
|
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
|
<script src="/static/htmx.min.js" defer></script>
|
|
<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 legacy.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 signed-out pages are narrower than the rest
|
|
(see main.page-* in legacy.css). It is .Active, already carried for the
|
|
navigation, rather than a second field every handler would have to
|
|
remember to set. The "wide" block beside it is how a page made of data
|
|
asks for the whole column instead of the reading measure; it is the page's
|
|
own declaration rather than a name in the stylesheet, since .Active cannot
|
|
tell the send log from a single delivery's page. */}}
|
|
<main class="page-{{.Active}} {{template "wide" .}}">
|
|
{{template "content" .}}
|
|
{{/* Appropriate Legal Notices (AGPL-3.0): copyright, how to read the licence,
|
|
where the Corresponding Source is, and that there is no warranty. Shown
|
|
on every page, including login/setup — those are interactive UIs too.
|
|
The running version stays signed-in only: it is what a backup manifest is
|
|
checked against on restore, and must not be advertised to the internet. */}}
|
|
<footer class="version">
|
|
{{if .User}}SelfPost {{.Version}} · {{end}}{{.Copyright}} ·
|
|
<a href="/license">License (AGPL-3.0)</a> ·
|
|
<a href="{{.SourceURL}}">Source</a> ·
|
|
No warranty
|
|
</footer>
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>{{end}}
|
|
|
|
{{/* nav is the panel's navigation bar. It is rendered here, from the layout, so
|
|
every authenticated page has it without the page's own template having to
|
|
remember to include it; .Active names the current page so it is highlighted
|
|
instead of linking to itself. Unauthenticated pages (login, setup) carry no
|
|
.User and get no nav. */}}
|
|
{{define "nav"}}
|
|
<nav class="nav">
|
|
{{/* The order of the blocks is the order they are read in, top to bottom of
|
|
the column: the mark, the panel's pages, and the session last. The mark
|
|
goes to /overview, the page the panel opens on — named outright rather
|
|
than as "/", which is only a redirect to it. */}}
|
|
<a class="brand" href="{{if .IsGlobal}}/overview{{else if .HasOutbound}}/outbound/domains{{else}}/inbound/domains{{end}}"><img src="/static/logo-compact.svg" width="220" height="100" alt="SelfPost"></a>
|
|
<div class="links">
|
|
{{if .IsGlobal}}
|
|
{{if eq .Active "status"}}<span aria-current="page">{{template "icon-status"}}Status</span>{{else}}<a href="/overview">{{template "icon-status"}}Status</a>{{end}}
|
|
{{end}}
|
|
{{/* A group with nothing assigned is absent from that user's menu: the
|
|
sending pages need outbound reach, Inbound needs inbound reach. */}}
|
|
{{if or .IsGlobal .HasOutbound}}
|
|
{{if eq .Active "domains"}}<span aria-current="page">{{template "icon-domains"}}Domains</span>{{else}}<a href="/outbound/domains">{{template "icon-domains"}}Domains</a>{{end}}
|
|
{{end}}
|
|
{{if and .InboundEnabled (or .IsGlobal .HasInbound)}}
|
|
{{if eq .Active "inbound"}}<span aria-current="page">{{template "icon-inbound"}}Inbound</span>{{else}}<a href="/inbound/domains">{{template "icon-inbound"}}Inbound</a>{{end}}
|
|
{{end}}
|
|
{{if and .DMARCEnabled (or .IsGlobal .HasOutbound)}}
|
|
{{if eq .Active "dmarc"}}<span aria-current="page">{{template "icon-dmarc"}}DMARC</span>{{else}}<a href="/outbound/dmarc">{{template "icon-dmarc"}}DMARC</a>{{end}}
|
|
{{end}}
|
|
{{if eq .Active "help"}}<span aria-current="page">{{template "icon-help"}}Help</span>{{else}}<a href="/help">{{template "icon-help"}}Help</a>{{end}}
|
|
{{if or .IsGlobal .HasOutbound}}
|
|
{{if eq .Active "deliveries"}}<span aria-current="page">{{template "icon-deliveries"}}Deliveries</span>{{else}}<a href="/outbound/log">{{template "icon-deliveries"}}Deliveries</a>{{end}}
|
|
{{end}}
|
|
{{if .IsGlobal}}
|
|
{{if eq .Active "mail_queue"}}<span aria-current="page">{{template "icon-mail-queue"}}Mail queue</span>{{else}}<a href="/outbound/queue">{{template "icon-mail-queue"}}Mail queue</a>{{end}}
|
|
{{if eq .Active "system_log"}}<span aria-current="page">{{template "icon-system-log"}}System log</span>{{else}}<a href="/server/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="/server/backup">{{template "icon-backup"}}Backup</a>{{end}}
|
|
{{if eq .Active "users"}}<span aria-current="page">{{template "icon-users"}}Users</span>{{else}}<a href="/server/users">{{template "icon-users"}}Users</a>{{end}}
|
|
{{end}}
|
|
</div>
|
|
<div class="session">
|
|
<span class="session-user muted">{{template "icon-account"}}User: {{.User}}</span>
|
|
{{/* Account is every user's own; Settings is the instance's and belongs
|
|
to the global role. */}}
|
|
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Account</span>{{else}}<a href="/account">{{template "icon-account"}}Account</a>{{end}}
|
|
{{if .IsGlobal}}
|
|
{{if eq .Active "settings"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/server/settings">{{template "icon-settings"}}Settings</a>{{end}}
|
|
{{end}}
|
|
<form class="inline" method="post" action="/logout">
|
|
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
|
</form>
|
|
</div>
|
|
</nav>
|
|
{{end}}
|
|
|
|
{{/* Whether the page takes the column whole. Empty here, so a page is held to
|
|
the reading measure unless it says otherwise; the pages that are tables of
|
|
data, raw log lines or side-by-side cards redefine this as the word "wide",
|
|
which lands in <main>'s class list (see main.wide in legacy.css). Each
|
|
page's template file is parsed after this one, so its definition replaces
|
|
the empty one. */}}
|
|
{{define "wide"}}{{end}}
|
|
|
|
{{/* back_link — up-navigation on drill-down pages. Invoke with the "back"
|
|
function, e.g. {{template "back_link" (back "/server/users" "Back to users")}}.
|
|
Place it directly under the page <h1> and above the cards (see .back in
|
|
legacy.css). TestDrillDownPagesPlaceBackLinkAboveContent guards this. */}}
|
|
{{define "back_link"}}
|
|
<a class="back" href="{{.Href}}">← {{.Label}}</a>
|
|
{{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
|
|
aria-hidden because the entry's own text is already the accessible name;
|
|
the icon is a landmark for the eye, not a second label. Kept as separate
|
|
templates so the nav above stays one readable line per page. */}}
|
|
{{define "icon-status"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.25 8.5h2.9L6.2 3.4l3.1 9.4 1.9-4.3h3.55"/></svg>{{end}}
|
|
{{define "icon-domains"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M1.9 8h12.2"/><path d="M8 1.75c1.85 1.8 2.8 4 2.8 6.25S9.85 12.45 8 14.25C6.15 12.45 5.2 10.25 5.2 8S6.15 3.55 8 1.75Z"/></svg>{{end}}
|
|
{{define "icon-inbound"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 9.5h11"/><path d="M8 2.75v6.2"/><path d="M5.4 6.4 8 9.05 10.6 6.4"/><path d="M3.2 12.6h9.6"/></svg>{{end}}
|
|
{{define "icon-dmarc"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 4.5h11v7H2.5z"/><path d="M5 7.5h6M5 9.5h4"/></svg>{{end}}
|
|
{{define "icon-help"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2V11.4"/><path d="M8 5.05v.01"/></svg>{{end}}
|
|
{{define "icon-deliveries"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.25 1.75 1.6 6.6l5 2.05 2.05 5z"/><path d="M14.25 1.75 6.6 8.65"/></svg>{{end}}
|
|
{{define "icon-mail-queue"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.75 9.5h3.3l1 1.75h3.9l1-1.75h3.3v3.05a1.2 1.2 0 0 1-1.2 1.2H2.95a1.2 1.2 0 0 1-1.2-1.2z"/><path d="M1.75 9.5 3.4 3.2a1.25 1.25 0 0 1 1.2-.95h6.8a1.25 1.25 0 0 1 1.2.95l1.65 6.3"/></svg>{{end}}
|
|
{{define "icon-system-log"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.75 1.75h5.1l3.4 3.4v8.05a1.05 1.05 0 0 1-1.05 1.05H3.75a1.05 1.05 0 0 1-1.05-1.05V2.8a1.05 1.05 0 0 1 1.05-1.05Z"/><path d="M8.85 1.75v3.4h3.4"/><path d="M5.35 8.6h5.3M5.35 11.1h3.5"/></svg>{{end}}
|
|
{{/* A filing cabinet rather than a storage box: the box read as a box, while
|
|
two drawers with handles is the picture of an archive. */}}
|
|
{{define "icon-backup"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="1.75" width="10.5" height="12.5" rx="1.15"/><path d="M2.75 8h10.5"/><path d="M6.4 4.85h3.2M6.4 11.15h3.2"/></svg>{{end}}
|
|
{{define "icon-account"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="5.4" r="2.75"/><path d="M2.9 14.25a5.1 5.1 0 0 1 10.2 0"/></svg>{{end}}
|
|
{{/* Two people: rear as a right-side crescent (head + shoulder) so it peeks
|
|
from behind; front is a full silhouette matching icon-account. Heads share
|
|
one baseline; shoulder arcs share one floor — otherwise the pair looks
|
|
lopsided at 16 px. */}}
|
|
{{define "icon-users"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.9 3.1a2.1 2.1 0 0 1 0 4.2"/><path d="M14.7 14.25a3.8 3.8 0 0 0-3.9-3.65"/><circle cx="5.5" cy="5.2" r="2.5"/><path d="M1.4 14.25a4.8 4.8 0 0 1 8.2 0"/></svg>{{end}}
|
|
{{/* A gear (Feather "settings"), not a sun-with-rays: at 16 px spokes alone
|
|
read as brightness, while the toothed rim is the picture of configuration.
|
|
viewBox 0 0 24 matches the source path; CSS sizes the icon to 1 rem. */}}
|
|
{{define "icon-settings"}}<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>{{end}}
|
|
{{/* Sign out is the one icon here that marks an action rather than a page: an
|
|
open door with the arrow leading out of it. */}}
|
|
{{define "icon-sign-out"}}<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.1 14.25H3.65a1.15 1.15 0 0 1-1.15-1.15V2.9a1.15 1.15 0 0 1 1.15-1.15H6.1"/><path d="M10.6 11.15 13.75 8 10.6 4.85"/><path d="M13.75 8H6.35"/></svg>{{end}}
|