panel-redesign: Server pages and Help on the kit; the help drawer is gone

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>
This commit is contained in:
mix
2026-10-10 02:14:25 +03:00
co-authored by Claude Opus 5.5
parent 610023ef6a
commit fa696efa8b
37 changed files with 1896 additions and 537 deletions
+43 -38
View File
@@ -1,43 +1,48 @@
{{/* Backup (mockup backup.html): the whole instance in one archive, and one
domain brought in from another SelfPost. Two boxes, each its own form and
its own POST; a refusal of either comes back as the flash above them. The
password fields of the full backup are shown when the box is ticked (panel.js
reads data-encrypt-toggle / data-encrypt-fields); the import asks for a
password only for an encrypted file (data-import-file /
data-import-password-fields). Without scripts all of them are visible. */}}
{{define "content"}}
<h1>Backup &amp; migration</h1>
{{template "page_head" .Head}}
{{if .Flash}}{{template "flash" .Flash}}{{end}}
<div class="card">
<h2>Full backup</h2>
<p class="muted">Download a self-contained backup of the whole instance —
<code>data/</code> (database, DKIM keys, application credentials, and the
Postfix queue), <code>docker-compose.yml</code>, <code>.env</code>, and
<code>certs/</code>. Extract it into an empty project directory on a new
machine, adjust hostname or proxy settings if needed, and start a container of
the <strong>same SelfPost version</strong> before first boot. The reverse-proxy
vhost is not included — set that up separately on the new host.</p>
<p class="muted"><strong>The backup file is a secret</strong> (it contains
private keys, TLS material, and credentials). Store and transfer it securely and delete it
once the restore succeeds. Encrypting it below is the simplest way to do that:
the download is then a <code>.spbk</code> file (SelfPost backup) that only
the password opens.</p>
{{if .BackupErr}}<p class="error">{{.BackupErr}}</p>{{end}}
<form method="post" action="/server/backup">
{{template "encryptfields" .}}
<button type="submit">Download full backup</button>
</form>
</div>
<div class="card">
<h2>Import a domain</h2>
<p class="muted">Move a single domain here from another SelfPost instance using
a domain export file (from that domain's page) — plain <code>.json</code> or
encrypted <code>.spde</code> (SelfPost domain export). Its DKIM key and
application passwords come across, so the published DNS record needs no
change. The export file is a secret, like a full backup.</p>
{{if .ImportErr}}<p class="error">{{.ImportErr}}</p>{{end}}
<form method="post" action="/server/backup/import" enctype="multipart/form-data">
<label for="importfile">Domain export file</label>
<input id="importfile" name="file" type="file" accept=".json,.spde,application/json" required data-import-file>
<div class="encrypt-fields" data-import-password-fields>
<label for="importpw">Password</label>
<input id="importpw" name="import_password" type="password" autocomplete="off">
<div class="columns">
<div class="column is-6">
{{template "box_open" .Full}}
{{template "body_open"}}
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials, the queue), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
<form class="sp-form" method="post" action="{{.FullAction}}">
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" value="1"{{if .Encrypt}} checked{{end}} data-encrypt-toggle> Encrypt with a password (<code>.spbk</code>)</label></div>
<div data-encrypt-fields>
<div class="sp-pair">
<div class="field"><label class="label" for="encpw">Password</label><div class="control"><input class="input" id="encpw" name="password" type="password" autocomplete="new-password" minlength="{{.PasswordMin}}" placeholder="at least {{.PasswordMin}} characters"></div></div>
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" name="password_confirm" type="password" autocomplete="new-password"></div></div>
</div>
<button type="submit">Import domain</button>
</form>
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
</div>
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
</form>
{{template "body_close"}}
{{template "box_close"}}
</div>
<div class="column is-6">
{{template "box_open" .Import}}
{{template "body_open"}}
<p class="mb-3">A domain export from another instance — plain <code>.json</code> or encrypted <code>.spde</code>. Its DKIM key and application passwords come across, so the published DNS needs no change. The export itself is made on the domain's settings page.</p>
<form class="sp-form" method="post" action="{{.ImportAction}}" enctype="multipart/form-data">
<div class="field"><label class="label" for="importfile">Domain export file</label>
<div class="control"><input class="input" id="importfile" name="file" type="file" accept=".json,.spde,application/json" required data-import-file></div></div>
<div class="field" data-import-password-fields><label class="label" for="importpw">Password</label>
<div class="control"><input class="input" id="importpw" name="import_password" type="password" autocomplete="off"></div>
<p class="help">Asked only for an encrypted <code>.spde</code> file.</p></div>
<button type="submit" class="button is-primary"><i class="ti ti-file-import"></i>Import domain</button>
</form>
{{template "body_close"}}
{{template "box_close"}}
</div>
</div>
{{end}}