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
+7 -8
View File
@@ -15,6 +15,7 @@ import (
"github.com/mixeme/selfpost/internal/secretfile"
"github.com/mixeme/selfpost/internal/store"
"github.com/mixeme/selfpost/internal/web/validate"
"github.com/mixeme/selfpost/internal/web/view"
)
// maxImportBytes caps a domain-import upload. A domain export is a small JSON
@@ -45,13 +46,11 @@ func (h *Handlers) renderBackupPage(w http.ResponseWriter, r *http.Request, stat
// password), importErr to the import card, so neither message appears under the
// wrong form.
func (h *Handlers) renderBackupPageWith(w http.ResponseWriter, r *http.Request, status int, importErr, backupErr string) {
data := h.pageBase(r)
data["Title"] = "SelfPost — backup & migration"
data["Active"] = "backup"
data["ImportErr"] = importErr
data["BackupErr"] = backupErr
data["MinPwLen"] = validate.MinSecretFilePasswordLen
h.view.Render(w, status, "backup", data)
refusal := importErr
if refusal == "" {
refusal = backupErr
}
h.view.Render(w, status, "backup", view.NewBackup(h.shellMeta(r), validate.MinSecretFilePasswordLen, refusal))
}
// HandleBackup streams a full-server backup as a download (architecture.md §
@@ -327,7 +326,7 @@ func decryptErrorMessage(err error) string {
// deployBackupErr phrases a pre-flight backup failure for the operator.
func deployBackupErr(err error) string {
if strings.Contains(err.Error(), "DeployRoot") || strings.Contains(err.Error(), "deploy root") {
return "Full backup needs the project directory mounted read-only at /selfpost-deploy — add <code>.:/selfpost-deploy:ro</code> to docker-compose.yml and recreate the container."
return "Full backup needs the project directory mounted read-only at /selfpost-deploy — add .:/selfpost-deploy:ro to the volumes in docker-compose.yml and recreate the container."
}
return "Could not create the backup: " + err.Error()
}