Stage 1, step 3 (plan § Routes). BREAKING: every panel URL changes and the
old paths are removed without redirects.
Routes (web.go)
- /status -> /overview; /domains -> /outbound/domains with the domain's
settings actions under .../settings/ (reports, ratelimit, export);
/deliveries -> /outbound/log; /mail-queue -> /outbound/queue; /dmarc ->
/outbound/dmarc; /inbound -> /inbound/domains; /system-log ->
/server/log; /backup -> /server/backup (import under it); /users ->
/server/users; recheck and reload under /server/health/.
- One fragment name: <page>/fragment everywhere.
- Everything under /server/ is registered through server(), which wraps
the handler in globalOnly: the subtree is guarded once, where a new
route cannot forget it. Handlers keep their own requireGlobal.
- GET / leads to /overview, /outbound/domains or /inbound/domains.
- The /account -> /settings redirect is gone with /settings.
Account and Server > Settings
- handlers_account.go: GET /account and three POSTs - profile (username,
e-mail; no password asked, as designed), password (current password,
rate-limited like a sign-in, signs out other sessions), dmarc (the
user's default: hosted / account / custom / none, each validated).
This replaces step 1's bridge through the old Settings form.
- handlers_settings.go is the instance's page only: log retention and the
read-only rate limits, global role.
- One legacy template serves both pages by a flag until stage 2 gives each
its own: a new legacy page cannot be added to the ratchet.
Still to come inside stage 1, and why @routes stays on the ratchet:
- step 4: the application routes (one POST, under the domain);
- step 5: GET /server/health.
With @routes removed by hand TestRoutesFollowNavigation reports exactly
those and nothing else - no template links to a path the router lacks.
Old templates, tests and the e2e client are retargeted. The e2e module was
edited but not run here (no Docker, and its dependency could not be
downloaded on this machine).
Tests: Account (page, profile, password, DMARC default in every mode, a
domain administrator's own row only), Server settings (saved without a
password, range, global only), the /server/ subtree answered 404 by the
router for a domain administrator with All on both lists and for no
principal.
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail.
Roadmap 13/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
14 KiB
HTML
161 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>
|
|
{{if .User}}{{template "help_inputs" .}}{{end}}
|
|
{{/* 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>
|
|
{{if .User}}{{template "help_drawer" .}}{{end}}
|
|
</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}}
|