Nothing in the UI said which build was running, though it is the value a
backup manifest is compared against on restore and the first thing worth
knowing when the panel misbehaves — it was only in the startup log line
and `panel -version`.
Add it as a small footer in the shared layout, supplied from render()
alongside .Active so no handler has to pass it, and gated on .User: the
login and setup pages face the internet and should not advertise a
version. Tests cover both the footer and render() supplying the key,
since neither is visible from any single handler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was a bare link trailing the queue-summary sentence, while the two
other card actions on the same page (Re-check DNS, Reload configuration)
are buttons. Pull it out of the paragraph and give it the filled button
style through a new a.btn class — the same base rule a.danger already
used, so an action that happens to be a navigation still looks like
every other action.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The side-by-side .split row read badly: main is capped at 48rem, so the
applications table had to live in roughly 27rem and its actions column
squeezed four controls into it.
Put the create form directly above the list instead — the order the
domains page already uses for "Add a sending domain" above "Domains" —
and delete .split, which nothing else used. The empty-state text follows
the same page's wording.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same kind of control, two appearances: a POST wrapped in form.inline
(Re-check, Export domain, Sign out, New password, Delete), the <details>
toggles in the applications table and the a.danger delete links all
rendered as bold blue/red text, while every other action was a filled
button — sometimes both within one card, as on the backup page where
"Download full backup" was text and "Import domain" right below it was a
button.
Give them one vocabulary: filled for a card's own action, and a compact
outlined variant (the style the Copy buttons already used) where actions
cluster in a table row or the nav bar. An <a> is now only used for
navigation. The <details> summary keeps the pressed background instead of
a disclosure marker, and the row buttons are nowrap so a narrow actions
column widens rather than wrapping every label onto two lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On the domain page the "Add an application" card sat below the domain rate
limit, so adding a login meant scrolling past the list and back. Move it up
next to the Applications table and wrap the pair in a .split grid (1.5fr /
1fr, so the table keeps the wider column). The columns collapse to one below
52rem, list first, and the grid gap keeps the same vertical rhythm as
.card + .card.
The empty-state text said "Create one below", which is no longer where the
form is; it now names the card instead of its position.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for the Content-Security-Policy of phase 14.A. A policy that has to
allow inline script is not worth writing — script-src 'unsafe-inline' gives
back exactly the XSS foothold the policy exists to remove — so the three
inline constructs the templates still had are moved out first:
- the layout's <style> block becomes /static/panel.css;
- the one style="background:#b42318" attribute becomes the .danger class
that already existed for it;
- the four onsubmit="return confirm(...)" handlers become data-confirm,
handled by a delegated listener in panel.js. Delegation matters: the
application rows are also delivered by HTMX swaps.
htmx would otherwise inject a <style> of its own for the request-indicator
classes and become the single reason the policy needs an exemption; the panel
uses no hx-indicator, so the meta config switches it off.
A guard test keeps this from silently regressing later, which it otherwise
would: an inline handler added to a template does not fail, it just quietly
stops working in the browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 12 (UI/UX). The navigation bar now renders once from layout.html
instead of being copied into each content template, so it is present on
every authenticated page — including the domain page and its delete
confirmation, which had no links at all — and the current page is
highlighted via .Active rather than quietly dropping out of the list.
New /account page changes the administrator's username and/or password:
the current password is required and the attempt is throttled on the same
limiter as the login form, so this route cannot be used to brute-force
past that limit. A password change invalidates every other session while
keeping the one performing it; a rename carries that session over.
Backup and domain import move from a card in the middle of the domain
list to their own /backup page, one card each; the handlers themselves
are unchanged, only the page the import form renders its errors on.
The domain page gains a "Sending server settings" card (server, port,
encryption) so a client can be configured without reading the docs; 587
is listed only when SUBMISSION_ENABLE is true for this deployment, which
is a deploy-time flag the panel cannot verify at runtime.
Client-side (static/panel.js, no libraries): Copy buttons on the values
that get carried elsewhere (DKIM record, new application credentials,
server name), and the Addresses field is hidden while the address mode is
wildcard, where the server ignores it.
Verified in a container on the dev server: setup, login, every page's
nav and active item, domain and application creation, all account-form
paths including cross-session invalidation, import errors, full backup
download. gofmt/vet/test/docker build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>