Step 6/12 of docs/plans/inbound-antispam-panel.md. Written by a Sonnet
subagent from the mockups, reviewed and corrected by me.
GET /inbound/log - the journal a page at a time (50), filtered by
domain, decision and day; QueryInboundSpam / CountInboundSpam with the
viewer's inbound domains as the scope. The global role reads all of it
and gets "Deny sender" / "Allow sender" links into the lists; a user
with the domain role reads the rows of the inbound domains assigned to
them, without the links. The plan said global-only for the journal;
the accepted mockup and 2.0's delegation of Inbound say otherwise -
recorded in the plan as built.
GET, POST /inbound/filter-lists and POST .../{id}/delete - global role,
404 otherwise. Add form (prefilled from the query), the two lists with
Remove as the kit's row action, and "Where the lists go" with the
filter's state and when the maps were last written
(Lists.LastWritten). A refused or duplicate entry is 422 with the
message and what was typed.
Server > Settings: "Keep inbound log rows, days" with the relay on.
Help: Inbound log, Filter lists. Navigation: Log for everyone who sees
Inbound, Filter lists for the global role (Shell.ShowFilterLists).
Status tags accept / reject / tempfail join the kit's mapping.
Tests: page fixtures for both screens, held to outlines.json; view and
handler tests (both roles, filters, paging, relay off, add / duplicate
/ invalid / delete, map file content, settings field, help). e2e reads
the journal from the page instead of the database and adds and removes
a deny entry through the panel, reading deny_domain.map after each.
Seen: both pages and Settings rendered from their fixtures and compared
with the mockups side by side (headless Edge, 1360 px) - same layout
and wording.
Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
11 KiB
HTML
151 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. */}}
|
|
<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}}
|