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:
@@ -44,6 +44,13 @@ var pageFixtures = map[string]func() any{
|
||||
"dmarc": func() any { return dmarcHubFixture() },
|
||||
"dmarc-domain": func() any { return dmarcDomainFixture() },
|
||||
"dmarc-report": func() any { return dmarcReportFixture() },
|
||||
|
||||
"system-log": func() any { return systemLogFixture() },
|
||||
"backup": func() any { return backupFixture() },
|
||||
"users": func() any { return usersFixture() },
|
||||
"user": func() any { return userFixture() },
|
||||
"user-delete": func() any { return userDeleteFixture() },
|
||||
"help": func() any { return NewHelp(admin(), true) },
|
||||
}
|
||||
|
||||
// admin is the signed-in global administrator every fixture is rendered for.
|
||||
@@ -342,3 +349,55 @@ func inDomainFixture() *InDomain {
|
||||
p.WithFilter(true, "inet:antispam:11332", "accept")
|
||||
return p
|
||||
}
|
||||
|
||||
// systemLogFixture is the mockup's System log: ten lines of mail.log, as the
|
||||
// handler reads them (oldest first), with the deferrals in amber and the
|
||||
// refusals and the bounce in red.
|
||||
func systemLogFixture() *SystemLog {
|
||||
return NewSystemLog(admin(), []LogLine{
|
||||
{Time: "Sep 21 13:00:00", Text: "opendkim[39]: key table reloaded (3 domains)"},
|
||||
{Time: "Sep 21 13:40:02", Text: "journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM", Level: "warn"},
|
||||
{Time: "Sep 21 13:41:19", Text: "postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)", Level: "error"},
|
||||
{Time: "Sep 21 13:44:02", Text: "postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)", Level: "warn"},
|
||||
{Time: "Sep 21 13:57:41", Text: "postfix/smtpd[2119]: NOQUEUE: reject: RCPT from bulk.example[198.51.100.90]: 550 5.1.1 <sales@lists.example.org>: Recipient address rejected: unknown recipient", Level: "error"},
|
||||
{Time: "Sep 21 13:58:09", Text: "postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, status=deferred (451 4.7.1 Greylisted, try again in 5 minutes)", Level: "warn"},
|
||||
{Time: "Sep 21 14:00:18", Text: "postfix/smtp[2130]: 4XcB8z5Tn1z1: to=<dev@lists.example.org>, relay=mx.internal[10.0.4.2]:25, status=sent (250 2.0.0 Ok: queued)"},
|
||||
{Time: "Sep 21 14:00:18", Text: "postfix/smtpd[2129]: connect from mail.uni.example[192.0.2.77]"},
|
||||
{Time: "Sep 21 14:01:52", Text: "postfix/qmgr[58]: 4XcB9q0Lw2z1: removed"},
|
||||
{Time: "Sep 21 14:01:52", Text: "postfix/smtp[2131]: 4XcB9q0Lw2z1: to=<m.keller@gmx.de>, relay=mx00.gmx.net[212.227.15.9]:25, delay=0.8, status=sent (250 Requested mail action okay)"},
|
||||
}, "")
|
||||
}
|
||||
|
||||
// backupFixture is the mockup's Backup: nothing refused, the password minimum
|
||||
// of the encryption fields at twelve.
|
||||
func backupFixture() *Backup {
|
||||
return NewBackup(admin(), 12, "")
|
||||
}
|
||||
|
||||
// usersFixture is the mockup's Users: the signed-in global administrator and
|
||||
// two domain administrators, one of them just created.
|
||||
func usersFixture() *Users {
|
||||
return NewUsers(admin(), true, "User created.").WithRows([]UserRow{
|
||||
NewUserRow(UserRowInput{ID: 1, Username: "admin", Email: "mix@example.org", Global: true, You: true}),
|
||||
NewUserRow(UserRowInput{ID: 2, Username: "shop-team", Email: "team@shop.example.org", Outbound: []string{"shop.example.org"}}),
|
||||
NewUserRow(UserRowInput{ID: 3, Username: "acme-ops", Email: "ops@acme.io", AllOutbound: true, Inbound: []string{"acme.io"}}),
|
||||
})
|
||||
}
|
||||
|
||||
// userFixture is the mockup's user form, filled with acme-ops: a domain
|
||||
// administrator on all outbound domains and on acme.io inbound.
|
||||
func userFixture() *UserForm {
|
||||
return NewUserForm(admin(), UserFormInput{
|
||||
ID: 3, Name: "acme-ops", Username: "acme-ops", Email: "ops@acme.io", Role: "domain", CanDelete: true,
|
||||
PasswordMin: 12, ShowInbound: true,
|
||||
AllOut: true,
|
||||
OutDomains: []DomainChoice{{ID: 1, Name: "example.org"}, {ID: 2, Name: "shop.example.org"}, {ID: 3, Name: "notify.acme.io"}},
|
||||
InDomains: []DomainChoice{{ID: 1, Name: "lists.example.org"}, {ID: 2, Name: "acme.io", Checked: true}},
|
||||
})
|
||||
}
|
||||
|
||||
// userDeleteFixture is the mockup's confirmation: acme-ops, who has all
|
||||
// outbound domains and acme.io inbound.
|
||||
func userDeleteFixture() *UserDelete {
|
||||
return NewUserDelete(admin(), UserDeleteInput{ID: 3, Username: "acme-ops", AllOutbound: true, Inbound: []string{"acme.io"}})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user